new file structure & koneksi ke DB
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/config"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/config"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api"
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,6 +36,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer l.Sync()
|
defer l.Sync()
|
||||||
|
|
||||||
|
// Initialize database connection
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true",
|
||||||
|
cfg.Database.User,
|
||||||
|
cfg.Database.Password,
|
||||||
|
cfg.Database.Host,
|
||||||
|
cfg.Database.Port,
|
||||||
|
cfg.Database.Name)
|
||||||
|
|
||||||
|
err = database.Initialize(
|
||||||
|
dsn,
|
||||||
|
cfg.Database.MaxOpenConns,
|
||||||
|
cfg.Database.MaxIdleConns,
|
||||||
|
time.Duration(cfg.Database.ConnMaxLifetimeMins)*time.Minute,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
l.Fatal("Failed to initialize database", zap.Error(err))
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
|
l.Info("Database connection established",
|
||||||
|
zap.String("host", cfg.Database.Host),
|
||||||
|
zap.Int("port", cfg.Database.Port),
|
||||||
|
zap.String("database", cfg.Database.Name))
|
||||||
|
|
||||||
// Setup router
|
// Setup router
|
||||||
router := api.SetupRouter(cfg, l)
|
router := api.SetupRouter(cfg, l)
|
||||||
|
|
||||||
|
|||||||
@@ -40,11 +40,14 @@ type Config struct {
|
|||||||
} `mapstructure:"shortlink"`
|
} `mapstructure:"shortlink"`
|
||||||
|
|
||||||
Database struct {
|
Database struct {
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
User string `mapstructure:"user"`
|
User string `mapstructure:"user"`
|
||||||
Password string `mapstructure:"password"`
|
Password string `mapstructure:"password"`
|
||||||
Name string `mapstructure:"name"`
|
Name string `mapstructure:"name"`
|
||||||
|
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||||
|
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||||
|
ConnMaxLifetimeMins int `mapstructure:"conn_max_lifetime_mins"`
|
||||||
} `mapstructure:"database"`
|
} `mapstructure:"database"`
|
||||||
|
|
||||||
AllowedOrigins []string `mapstructure:"allowed_origins"`
|
AllowedOrigins []string `mapstructure:"allowed_origins"`
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ auth:
|
|||||||
jwt_secret: "vQ6PQqUyh7pBNOytClgN+Nw1XBq7F8Qo6VP3VwIqvHY=" # Change this in production!
|
jwt_secret: "vQ6PQqUyh7pBNOytClgN+Nw1XBq7F8Qo6VP3VwIqvHY=" # Change this in production!
|
||||||
access_token_expiry: 1440 # minutes (24 hours)
|
access_token_expiry: 1440 # minutes (24 hours)
|
||||||
refresh_token_expiry: 168 # hours (7 days)
|
refresh_token_expiry: 168 # hours (7 days)
|
||||||
enable_database_auth: false # Set to true when ready to use database
|
enable_database_auth: true # Changed to true to use database
|
||||||
|
|
||||||
shortlink:
|
shortlink:
|
||||||
base_url: "http://localhost:3333" # The base URL for generated OHIF Auth shortlinks
|
base_url: "http://localhost:3333" # The base URL for generated OHIF Auth shortlinks
|
||||||
@@ -27,9 +27,12 @@ shortlink:
|
|||||||
database:
|
database:
|
||||||
host: "localhost"
|
host: "localhost"
|
||||||
port: 3306
|
port: 3306
|
||||||
user: "dbuser"
|
user: "root"
|
||||||
password: "dbpassword" # Consider using environment variables for sensitive data
|
password: "alfandi102938" # Change this to your MariaDB password
|
||||||
name: "ohif_proxy"
|
name: "ohif_proxy"
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 5
|
||||||
|
conn_max_lifetime_mins: 60
|
||||||
|
|
||||||
allowed_origins:
|
allowed_origins:
|
||||||
- "*" # For development; restrict this in production
|
- "*" # For development; restrict this in production
|
||||||
8
internal/api/models/doctor.go
Normal file
8
internal/api/models/doctor.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// DoctorDetails contains doctor-specific data
|
||||||
|
type DoctorDetails struct {
|
||||||
|
DoctorID string `json:"doctor_id"`
|
||||||
|
DoctorName string `json:"doctor_name"`
|
||||||
|
Type string `json:"type"` // "ref_doctor" or "expertise_doctor"
|
||||||
|
}
|
||||||
18
internal/api/models/patient.go
Normal file
18
internal/api/models/patient.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// PatientDetails contains patient-specific data
|
||||||
|
type PatientDetails struct {
|
||||||
|
PatientID string `json:"patient_id"`
|
||||||
|
PatientName string `json:"patient_name"`
|
||||||
|
StudyInstanceUIDs []string `json:"study_instance_uids,omitempty"`
|
||||||
|
AccessionNumbers []string `json:"accession_numbers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Study represents a DICOM study associated with a patient
|
||||||
|
type Study struct {
|
||||||
|
StudyInstanceUID string `json:"study_instance_uid"`
|
||||||
|
AccessionNumber string `json:"accession_number,omitempty"`
|
||||||
|
StudyDate string `json:"study_date,omitempty"`
|
||||||
|
StudyDescription string `json:"study_description,omitempty"`
|
||||||
|
Modalities string `json:"modalities,omitempty"`
|
||||||
|
}
|
||||||
52
internal/api/models/shortlink.go
Normal file
52
internal/api/models/shortlink.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
@@ -21,21 +21,6 @@ type RefreshToken struct {
|
|||||||
CreatedAt string `db:"created_at" json:"created_at"`
|
CreatedAt string `db:"created_at" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatientDetails contains patient-specific data
|
|
||||||
type PatientDetails struct {
|
|
||||||
PatientID string `json:"patient_id"`
|
|
||||||
PatientName string `json:"patient_name"`
|
|
||||||
StudyInstanceUIDs []string `json:"study_instance_uids,omitempty"`
|
|
||||||
AccessionNumbers []string `json:"accession_numbers,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoctorDetails contains doctor-specific data
|
|
||||||
type DoctorDetails struct {
|
|
||||||
DoctorID string `json:"doctor_id"`
|
|
||||||
DoctorName string `json:"doctor_name"`
|
|
||||||
Type string `json:"type"` // "ref_doctor" or "expertise_doctor"
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginRequest represents the login form data
|
// LoginRequest represents the login form data
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@@ -59,54 +44,3 @@ type RefreshRequest struct {
|
|||||||
type RefreshResponse struct {
|
type RefreshResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
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"`
|
|
||||||
}
|
|
||||||
|
|||||||
44
internal/api/repository/doctor.go
Normal file
44
internal/api/repository/doctor.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DBDoctor represents a doctor from the database
|
||||||
|
type DBDoctor struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Doctor_UsersID string `db:"Doctor_UsersID"`
|
||||||
|
DoctorID string `db:"DoctorID"`
|
||||||
|
DoctorName string `db:"DoctorName"`
|
||||||
|
DoctorCreatedAt time.Time `db:"DoctorCreatedAt"`
|
||||||
|
DoctorLastUpdatedAt time.Time `db:"DoctorLastUpdatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoctorRepository handles database operations related to doctors
|
||||||
|
type DoctorRepository struct {
|
||||||
|
*Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDoctorRepository creates a new doctor repository
|
||||||
|
func NewDoctorRepository() *DoctorRepository {
|
||||||
|
return &DoctorRepository{
|
||||||
|
Repository: NewRepository(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDoctorDetailsByUserID retrieves doctor details for a user
|
||||||
|
func (r *DoctorRepository) GetDoctorDetailsByUserID(userID string) (*DBDoctor, error) {
|
||||||
|
var dbDoctor DBDoctor
|
||||||
|
|
||||||
|
query := `SELECT * FROM doctor WHERE Doctor_UsersID = ?`
|
||||||
|
err := database.DB.Get(&dbDoctor, query, userID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("database error getting doctor details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dbDoctor, nil
|
||||||
|
}
|
||||||
62
internal/api/repository/patient.go
Normal file
62
internal/api/repository/patient.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DBPatient represents a patient from the database
|
||||||
|
type DBPatient struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Patient_UsersID string `db:"Patient_UsersID"`
|
||||||
|
PatientMedrec string `db:"PatientMedrec"`
|
||||||
|
PatientName string `db:"PatientName"`
|
||||||
|
PatientDoB time.Time `db:"PatientDoB"`
|
||||||
|
PatientCreatedAt time.Time `db:"PatientCreatedAt"`
|
||||||
|
PatientUpdatedAt time.Time `db:"PatientUpdatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatientRepository handles database operations related to patients
|
||||||
|
type PatientRepository struct {
|
||||||
|
*Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPatientRepository creates a new patient repository
|
||||||
|
func NewPatientRepository() *PatientRepository {
|
||||||
|
return &PatientRepository{
|
||||||
|
Repository: NewRepository(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPatientDetailsByUserID retrieves patient details for a user
|
||||||
|
func (r *PatientRepository) GetPatientDetailsByUserID(userID string) (*models.PatientDetails, error) {
|
||||||
|
var dbPatient DBPatient
|
||||||
|
|
||||||
|
query := `SELECT * FROM patient WHERE Patient_UsersID = ?`
|
||||||
|
err := database.DB.Get(&dbPatient, query, userID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("database error getting patient details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create StudyRepository to get patient studies
|
||||||
|
studyRepo := NewStudyRepository()
|
||||||
|
studyUIDs, accessionNumbers, err := studyRepo.GetPatientStudies(dbPatient.PatientMedrec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.PatientDetails{
|
||||||
|
PatientID: dbPatient.PatientMedrec,
|
||||||
|
PatientName: dbPatient.PatientName,
|
||||||
|
StudyInstanceUIDs: studyUIDs,
|
||||||
|
AccessionNumbers: accessionNumbers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
23
internal/api/repository/repository.go
Normal file
23
internal/api/repository/repository.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repository provides a base interface to the database
|
||||||
|
type Repository struct {
|
||||||
|
db *sqlx.DB // Changed from *sql.DB to *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRepository creates a new database repository
|
||||||
|
func NewRepository() *Repository {
|
||||||
|
return &Repository{
|
||||||
|
db: database.DB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the database connection (no-op as DB is managed by database package)
|
||||||
|
func (r *Repository) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
141
internal/api/repository/shortlink.go
Normal file
141
internal/api/repository/shortlink.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||||
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DBShortLink represents a shortlink from the database
|
||||||
|
type DBShortLink struct {
|
||||||
|
ShortlinkID int `db:"ShortlinkID"`
|
||||||
|
ShortlinKCode string `db:"ShortlinKCode"`
|
||||||
|
Shortlink_PatientID string `db:"Shortlink_PatientID"`
|
||||||
|
Shortlink_Study_IUID string `db:"Shortlink_Study_IUID"`
|
||||||
|
ShortlinkHashDoB string `db:"ShortlinkHashDoB"`
|
||||||
|
ShortlinkExpiredAt time.Time `db:"ShortlinkExpiredAt"`
|
||||||
|
ShortlinkIsRevoked bool `db:"ShortlinkIsRevoked"`
|
||||||
|
ShortlinkRemainingTries int `db:"ShortlinkRemainingTries"`
|
||||||
|
ShortlinkCreatedAt time.Time `db:"ShortlinkCreatedAt"`
|
||||||
|
ShortlinkCreate_UserID int `db:"ShortlinkCreate_UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortLinkRepository handles database operations related to shortlinks
|
||||||
|
type ShortLinkRepository struct {
|
||||||
|
*Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewShortLinkRepository creates a new shortlink repository
|
||||||
|
func NewShortLinkRepository() *ShortLinkRepository {
|
||||||
|
return &ShortLinkRepository{
|
||||||
|
Repository: NewRepository(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToShortLink converts a DBShortLink to a ShortLink model
|
||||||
|
func (s *DBShortLink) ToShortLink() *models.ShortLink {
|
||||||
|
return &models.ShortLink{
|
||||||
|
ID: fmt.Sprintf("%d", s.ShortlinkID),
|
||||||
|
Token: s.ShortlinKCode,
|
||||||
|
PatientID: s.Shortlink_PatientID,
|
||||||
|
StudyUID: s.Shortlink_Study_IUID,
|
||||||
|
HashedDOB: s.ShortlinkHashDoB,
|
||||||
|
ExpiresAt: s.ShortlinkExpiredAt.Format(time.RFC3339),
|
||||||
|
IsRevoked: s.ShortlinkIsRevoked,
|
||||||
|
RemainingTries: s.ShortlinkRemainingTries,
|
||||||
|
CreatedAt: s.ShortlinkCreatedAt.Format(time.RFC3339),
|
||||||
|
CreatedByID: fmt.Sprintf("%d", s.ShortlinkCreate_UserID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShortLinkByToken retrieves a shortlink by token
|
||||||
|
func (r *ShortLinkRepository) GetShortLinkByToken(token string) (*models.ShortLink, error) {
|
||||||
|
var dbShortLink DBShortLink
|
||||||
|
|
||||||
|
query := `SELECT * FROM shortlink WHERE ShortlinKCode = ?`
|
||||||
|
err := database.DB.Get(&dbShortLink, query, token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("database error getting shortlink: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbShortLink.ToShortLink(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateShortLink stores a new shortlink in the database
|
||||||
|
func (r *ShortLinkRepository) CreateShortLink(shortLink *models.ShortLink) error {
|
||||||
|
query := `INSERT INTO shortlink (
|
||||||
|
ShortlinKCode,
|
||||||
|
Shortlink_PatientID,
|
||||||
|
Shortlink_Study_IUID,
|
||||||
|
ShortlinkHashDoB,
|
||||||
|
ShortlinkExpiredAt,
|
||||||
|
ShortlinkIsRevoked,
|
||||||
|
ShortlinkRemainingTries,
|
||||||
|
ShortlinkCreatedAt,
|
||||||
|
ShortlinkCreate_UserID)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?)`
|
||||||
|
|
||||||
|
createdByID, err := strconv.Atoi(shortLink.CreatedByID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid created by ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid expiration date: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = database.DB.Exec(
|
||||||
|
query,
|
||||||
|
shortLink.Token,
|
||||||
|
shortLink.PatientID,
|
||||||
|
shortLink.StudyUID,
|
||||||
|
shortLink.HashedDOB,
|
||||||
|
expiresAt,
|
||||||
|
shortLink.IsRevoked,
|
||||||
|
shortLink.RemainingTries,
|
||||||
|
createdByID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error creating shortlink: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateShortLink updates an existing shortlink in the database
|
||||||
|
func (r *ShortLinkRepository) UpdateShortLink(shortLink *models.ShortLink) error {
|
||||||
|
query := `UPDATE shortlink SET
|
||||||
|
ShortlinkIsRevoked = ?,
|
||||||
|
ShortlinkRemainingTries = ?,
|
||||||
|
ShortlinkExpiredAt = ?
|
||||||
|
WHERE ShortlinKCode = ?`
|
||||||
|
|
||||||
|
expiresAt, err := time.Parse(time.RFC3339, shortLink.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid expiration date: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = database.DB.Exec(
|
||||||
|
query,
|
||||||
|
shortLink.IsRevoked,
|
||||||
|
shortLink.RemainingTries,
|
||||||
|
expiresAt,
|
||||||
|
shortLink.Token,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error updating shortlink: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
69
internal/api/repository/study.go
Normal file
69
internal/api/repository/study.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StudyRepository handles database operations related to studies
|
||||||
|
type StudyRepository struct {
|
||||||
|
*Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStudyRepository creates a new study repository
|
||||||
|
func NewStudyRepository() *StudyRepository {
|
||||||
|
return &StudyRepository{
|
||||||
|
Repository: NewRepository(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPatientStudies retrieves all studies for a patient
|
||||||
|
func (r *StudyRepository) GetPatientStudies(patientID string) ([]string, []string, error) {
|
||||||
|
var studies []DBStudy
|
||||||
|
|
||||||
|
query := `SELECT * FROM study WHERE Study_PatientID = ?`
|
||||||
|
err := database.DB.Select(&studies, query, patientID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("database error getting patient studies: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var studyUIDs []string
|
||||||
|
var accessionNumbers []string
|
||||||
|
|
||||||
|
for _, study := range studies {
|
||||||
|
studyUIDs = append(studyUIDs, study.Study_IUID)
|
||||||
|
if study.Study_AccessionNumber != "" {
|
||||||
|
accessionNumbers = append(accessionNumbers, study.Study_AccessionNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return studyUIDs, accessionNumbers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStudyByUID retrieves a study by its UID
|
||||||
|
func (r *StudyRepository) GetStudyByUID(studyUID string) (*DBStudy, error) {
|
||||||
|
var study DBStudy
|
||||||
|
|
||||||
|
query := `SELECT * FROM study WHERE Study_IUID = ?`
|
||||||
|
err := database.DB.Get(&study, query, studyUID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("database error getting study: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &study, nil
|
||||||
|
}
|
||||||
140
internal/api/repository/user.go
Normal file
140
internal/api/repository/user.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DBUser represents a user from the database
|
||||||
|
type DBUser struct {
|
||||||
|
UserID int `db:"UserID"`
|
||||||
|
UserEmail string `db:"UserEmail"`
|
||||||
|
UserPassword string `db:"UserPassword"`
|
||||||
|
UserRole string `db:"UserRole"`
|
||||||
|
UserName string `db:"UserName"`
|
||||||
|
UserCreatedAt time.Time `db:"UserCreatedAt"`
|
||||||
|
UserUpdatedAt time.Time `db:"UserUpdatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBRefreshToken represents a refresh token from the database
|
||||||
|
type DBRefreshToken struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Token string `db:"token"`
|
||||||
|
UserID string `db:"user_id"`
|
||||||
|
ExpiresAt time.Time `db:"expires_at"`
|
||||||
|
IsRevoked bool `db:"is_revoked"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserRepository handles database operations related to users
|
||||||
|
type UserRepository struct {
|
||||||
|
*Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserRepository creates a new user repository
|
||||||
|
func NewUserRepository() *UserRepository {
|
||||||
|
return &UserRepository{
|
||||||
|
Repository: NewRepository(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToUser converts a DBUser to a User model
|
||||||
|
func (u *DBUser) ToUser() *models.User {
|
||||||
|
return &models.User{
|
||||||
|
ID: fmt.Sprintf("%d", u.UserID),
|
||||||
|
Email: u.UserEmail,
|
||||||
|
Password: u.UserPassword,
|
||||||
|
Role: u.UserRole,
|
||||||
|
Name: u.UserName,
|
||||||
|
CreatedAt: u.UserCreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: u.UserUpdatedAt.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail retrieves a user by email
|
||||||
|
func (r *UserRepository) GetUserByEmail(email string) (*models.User, error) {
|
||||||
|
var dbUser DBUser
|
||||||
|
|
||||||
|
query := `SELECT * FROM user WHERE UserEmail = ?`
|
||||||
|
err := database.DB.Get(&dbUser, query, email)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("database error getting user by email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbUser.ToUser(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID retrieves a user by ID
|
||||||
|
func (r *UserRepository) GetUserByID(id string) (*models.User, error) {
|
||||||
|
var dbUser DBUser
|
||||||
|
|
||||||
|
query := `SELECT * FROM user WHERE UserID = ?`
|
||||||
|
err := database.DB.Get(&dbUser, query, id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("database error getting user by ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbUser.ToUser(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreRefreshToken saves a refresh token to the database
|
||||||
|
func (r *UserRepository) StoreRefreshToken(userID string, token string, expiresAt time.Time) error {
|
||||||
|
query := `INSERT INTO refresh_tokens (token, user_id, expires_at, is_revoked, created_at)
|
||||||
|
VALUES (?, ?, ?, false, NOW())`
|
||||||
|
|
||||||
|
_, err := database.DB.Exec(query, token, userID, expiresAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error storing refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRefreshToken retrieves a refresh token from the database
|
||||||
|
func (r *UserRepository) GetRefreshToken(token string) (*models.RefreshToken, error) {
|
||||||
|
var dbToken DBRefreshToken
|
||||||
|
|
||||||
|
query := `SELECT * FROM refresh_tokens WHERE token = ?`
|
||||||
|
err := database.DB.Get(&dbToken, query, token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("database error getting refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.RefreshToken{
|
||||||
|
ID: fmt.Sprintf("%d", dbToken.ID),
|
||||||
|
UserID: dbToken.UserID,
|
||||||
|
Token: dbToken.Token,
|
||||||
|
ExpiresAt: dbToken.ExpiresAt.Format(time.RFC3339),
|
||||||
|
IsRevoked: dbToken.IsRevoked,
|
||||||
|
CreatedAt: dbToken.CreatedAt.Format(time.RFC3339),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeRefreshToken marks a refresh token as revoked
|
||||||
|
func (r *UserRepository) RevokeRefreshToken(token string) error {
|
||||||
|
query := `UPDATE refresh_tokens SET is_revoked = true WHERE token = ?`
|
||||||
|
_, err := database.DB.Exec(query, token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database error revoking refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -53,7 +53,8 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
|||||||
// Initialize JWT auth service
|
// Initialize JWT auth service
|
||||||
jwtSecret := cfg.Auth.JWTSecret
|
jwtSecret := cfg.Auth.JWTSecret
|
||||||
if jwtSecret == "" {
|
if jwtSecret == "" {
|
||||||
logger.Warn("JWT secret not provided in config")
|
logger.Warn("JWT secret not provided in config, using default value. This is insecure for production!")
|
||||||
|
jwtSecret = "vQ6PQqUyh7pBNOytClgN+Nw1XBq7F8Qo6VP3VwIqvHY="
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert config values to time.Duration
|
// Convert config values to time.Duration
|
||||||
@@ -62,6 +63,8 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
|||||||
|
|
||||||
// Create JWT manager with config values
|
// Create JWT manager with config values
|
||||||
jwtManager := auth.NewJWTManager(jwtSecret, accessExpiry, refreshExpiry)
|
jwtManager := auth.NewJWTManager(jwtSecret, accessExpiry, refreshExpiry)
|
||||||
|
|
||||||
|
// Initialize services with domain-specific repositories
|
||||||
authService := service.NewAuthService(jwtManager)
|
authService := service.NewAuthService(jwtManager)
|
||||||
|
|
||||||
// Initialize shortlink service with config values
|
// Initialize shortlink service with config values
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"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/auth"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,26 +20,37 @@ var (
|
|||||||
|
|
||||||
// AuthService handles authentication operations
|
// AuthService handles authentication operations
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
jwtManager *auth.JWTManager
|
jwtManager *auth.JWTManager
|
||||||
// When you implement database connection, add a db client here
|
userRepo *repository.UserRepository
|
||||||
// db *sqlx.DB
|
patientRepo *repository.PatientRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthService creates a new authentication service
|
// NewAuthService creates a new authentication service
|
||||||
func NewAuthService(jwtManager *auth.JWTManager) *AuthService {
|
func NewAuthService(jwtManager *auth.JWTManager) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
jwtManager: jwtManager,
|
jwtManager: jwtManager,
|
||||||
|
userRepo: repository.NewUserRepository(),
|
||||||
|
patientRepo: repository.NewPatientRepository(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login authenticates a user and generates tokens
|
// Login authenticates a user and generates tokens
|
||||||
func (s *AuthService) Login(email, password string) (*models.LoginResponse, error) {
|
func (s *AuthService) Login(email, password string) (*models.LoginResponse, error) {
|
||||||
// Find user in mock data
|
// Find user in database
|
||||||
user := models.FindUserByCredentials(email, password)
|
user, err := s.userRepo.GetUserByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error finding user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
if err := CheckPassword(password, user.Password); err != nil {
|
||||||
|
return nil, ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
|
||||||
// Create token claims based on user role
|
// Create token claims based on user role
|
||||||
additionalClaims := make(map[string]interface{})
|
additionalClaims := make(map[string]interface{})
|
||||||
var redirectURL string
|
var redirectURL string
|
||||||
@@ -45,7 +58,11 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
|||||||
switch user.Role {
|
switch user.Role {
|
||||||
case "patient":
|
case "patient":
|
||||||
// Get patient data
|
// Get patient data
|
||||||
patientData := models.FindPatientDataByUserID(user.ID)
|
patientData, err := s.patientRepo.GetPatientDetailsByUserID(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting patient details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if patientData == nil {
|
if patientData == nil {
|
||||||
return nil, ErrUserNotFound
|
return nil, ErrUserNotFound
|
||||||
}
|
}
|
||||||
@@ -53,15 +70,15 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
|||||||
// Set patient-specific claims
|
// Set patient-specific claims
|
||||||
additionalClaims["patient_id"] = patientData.PatientID
|
additionalClaims["patient_id"] = patientData.PatientID
|
||||||
additionalClaims["patient_name"] = patientData.PatientName
|
additionalClaims["patient_name"] = patientData.PatientName
|
||||||
additionalClaims["study_iuids"] = patientData.StudyIUIDs
|
additionalClaims["study_iuids"] = patientData.StudyInstanceUIDs
|
||||||
additionalClaims["accession_numbers"] = patientData.AccessionNumbers
|
additionalClaims["accession_numbers"] = patientData.AccessionNumbers
|
||||||
|
|
||||||
// For redirectURL and home_url, use first study for simplicity
|
// For redirectURL and home_url, use first study for simplicity
|
||||||
if len(patientData.StudyIUIDs) > 0 {
|
if len(patientData.StudyInstanceUIDs) > 0 {
|
||||||
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyIUIDs[0])
|
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyInstanceUIDs[0])
|
||||||
redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyIUIDs[0])
|
redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyInstanceUIDs[0])
|
||||||
} else {
|
} else {
|
||||||
// Fallback for empty studies array (shouldn't happen)
|
// Fallback for empty studies array
|
||||||
additionalClaims["home_url"] = "/"
|
additionalClaims["home_url"] = "/"
|
||||||
redirectURL = "/"
|
redirectURL = "/"
|
||||||
}
|
}
|
||||||
@@ -79,7 +96,7 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
|||||||
|
|
||||||
redirectURL = "/"
|
redirectURL = "/"
|
||||||
|
|
||||||
case "expertise_doctor":
|
case "expertise_doctor", "admin":
|
||||||
// Expertise doctors have full access
|
// Expertise doctors have full access
|
||||||
additionalClaims["home_url"] = "/"
|
additionalClaims["home_url"] = "/"
|
||||||
additionalClaims["study_list"] = "enabled"
|
additionalClaims["study_list"] = "enabled"
|
||||||
@@ -98,6 +115,12 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store refresh token in database
|
||||||
|
expiresAt := time.Now().Add(s.jwtManager.GetRefreshExpiry())
|
||||||
|
if err := s.userRepo.StoreRefreshToken(user.ID, refreshToken, expiresAt); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to store refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return &models.LoginResponse{
|
return &models.LoginResponse{
|
||||||
AccessToken: accessToken,
|
AccessToken: accessToken,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
@@ -108,6 +131,27 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
|||||||
|
|
||||||
// RefreshToken generates a new access token using a refresh token
|
// RefreshToken generates a new access token using a refresh token
|
||||||
func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
|
func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
|
||||||
|
// Check if token exists and is not revoked
|
||||||
|
dbToken, err := s.userRepo.GetRefreshToken(refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get refresh token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dbToken == nil || dbToken.IsRevoked {
|
||||||
|
return "", errors.New("invalid or revoked refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse expiry time
|
||||||
|
expiresAt, err := time.Parse(time.RFC3339, dbToken.ExpiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("invalid token expiry format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired
|
||||||
|
if time.Now().After(expiresAt) {
|
||||||
|
return "", auth.ErrExpiredToken
|
||||||
|
}
|
||||||
|
|
||||||
// Validate the refresh token
|
// Validate the refresh token
|
||||||
claims, err := s.jwtManager.ValidateToken(refreshToken)
|
claims, err := s.jwtManager.ValidateToken(refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,7 +192,13 @@ func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new access token with the same claims
|
// Generate a new access token with the same claims
|
||||||
accessToken, err := s.jwtManager.GenerateAccessToken(claims.UserID, claims.Email, claims.Role, claims.UserName, additionalClaims)
|
accessToken, err := s.jwtManager.GenerateAccessToken(
|
||||||
|
claims.UserID,
|
||||||
|
claims.Email,
|
||||||
|
claims.Role,
|
||||||
|
claims.UserName,
|
||||||
|
additionalClaims,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -161,6 +211,11 @@ func (s *AuthService) ValidateToken(token string) (*auth.CustomClaims, error) {
|
|||||||
return s.jwtManager.ValidateToken(token)
|
return s.jwtManager.ValidateToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logout revokes a refresh token
|
||||||
|
func (s *AuthService) Logout(refreshToken string) error {
|
||||||
|
return s.userRepo.RevokeRefreshToken(refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
// HashPassword hashes a password using bcrypt
|
// HashPassword hashes a password using bcrypt
|
||||||
func HashPassword(password string) (string, error) {
|
func HashPassword(password string) (string, error) {
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
@@ -174,17 +229,3 @@ func HashPassword(password string) (string, error) {
|
|||||||
func CheckPassword(password, hash string) error {
|
func CheckPassword(password, hash string) error {
|
||||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Below functions would be implemented when connecting to a real database
|
|
||||||
|
|
||||||
// storeRefreshToken stores a refresh token in the database
|
|
||||||
func (s *AuthService) storeRefreshToken(userID, token string) error {
|
|
||||||
// TODO: In a real implementation, this would insert a record in the database
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// revokeRefreshToken marks a refresh token as revoked
|
|
||||||
func (s *AuthService) revokeRefreshToken(token string) error {
|
|
||||||
// TODO: In a real implementation, this would update a record in the database
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql" // Hanya menjalankan side-effectsnya saja dahulu
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Repository provides an interface to the database
|
|
||||||
type Repository struct {
|
|
||||||
db *sqlx.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRepository creates a new database repository
|
|
||||||
func NewRepository(dsn string) (*Repository, error) {
|
|
||||||
db, err := sqlx.Connect("mysql", dsn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set connection pool settings
|
|
||||||
db.SetMaxOpenConns(10)
|
|
||||||
db.SetMaxIdleConns(5)
|
|
||||||
db.SetConnMaxLifetime(time.Hour)
|
|
||||||
|
|
||||||
return &Repository{
|
|
||||||
db: db,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByEmail retrieves a user by email
|
|
||||||
func (r *Repository) GetUserByEmail(email string) (*models.User, error) {
|
|
||||||
var user models.User
|
|
||||||
err := r.db.Get(&user, "SELECT * FROM users WHERE email = ?", email)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByID retrieves a user by ID
|
|
||||||
func (r *Repository) GetUserByID(id string) (*models.User, error) {
|
|
||||||
var user models.User
|
|
||||||
err := r.db.Get(&user, "SELECT * FROM users WHERE id = ?", id)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StoreRefreshToken saves a refresh token to the database
|
|
||||||
func (r *Repository) StoreRefreshToken(userID, token string, expiresAt time.Time) error {
|
|
||||||
refreshToken := models.RefreshToken{
|
|
||||||
ID: uuid.New().String(),
|
|
||||||
UserID: userID,
|
|
||||||
Token: token,
|
|
||||||
ExpiresAt: expiresAt.Format(time.RFC3339),
|
|
||||||
IsRevoked: false,
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := r.db.NamedExec(
|
|
||||||
`INSERT INTO refresh_tokens (id, user_id, token, expires_at, is_revoked, created_at)
|
|
||||||
VALUES (:id, :user_id, :token, :expires_at, :is_revoked, :created_at)`,
|
|
||||||
refreshToken,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRefreshToken retrieves a refresh token from the database
|
|
||||||
func (r *Repository) GetRefreshToken(token string) (*models.RefreshToken, error) {
|
|
||||||
var refreshToken models.RefreshToken
|
|
||||||
err := r.db.Get(&refreshToken, "SELECT * FROM refresh_tokens WHERE token = ?", token)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &refreshToken, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RevokeRefreshToken marks a refresh token as revoked
|
|
||||||
func (r *Repository) RevokeRefreshToken(token string) error {
|
|
||||||
_, err := r.db.Exec("UPDATE refresh_tokens SET is_revoked = true WHERE token = ?", token)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPatientDetails retrieves patient details for a user
|
|
||||||
func (r *Repository) GetPatientDetails(userID string) (*models.PatientDetails, error) {
|
|
||||||
var patientDetails models.PatientDetails
|
|
||||||
err := r.db.Get(&patientDetails, "SELECT * FROM patient_details WHERE user_id = ?", userID)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &patientDetails, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDoctorDetails retrieves doctor details for a user
|
|
||||||
func (r *Repository) GetDoctorDetails(userID string) (*models.DoctorDetails, error) {
|
|
||||||
var doctorDetails models.DoctorDetails
|
|
||||||
err := r.db.Get(&doctorDetails, "SELECT * FROM doctor_details WHERE user_id = ?", userID)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &doctorDetails, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the database connection
|
|
||||||
func (r *Repository) Close() error {
|
|
||||||
return r.db.Close()
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/auth"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -38,14 +39,14 @@ var (
|
|||||||
|
|
||||||
// ShortLinkService handles operations related to short links
|
// ShortLinkService handles operations related to short links
|
||||||
type ShortLinkService struct {
|
type ShortLinkService struct {
|
||||||
jwtManager *auth.JWTManager
|
jwtManager *auth.JWTManager
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
shortLinkRepo *repository.ShortLinkRepository
|
||||||
|
patientRepo *repository.PatientRepository
|
||||||
// Configuration settings
|
// Configuration settings
|
||||||
baseURL string
|
baseURL string
|
||||||
defaultExpiryTime time.Duration
|
defaultExpiryTime time.Duration
|
||||||
maxAttempts int
|
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
|
// NewShortLinkService creates a new short link service
|
||||||
@@ -66,10 +67,11 @@ func NewShortLinkService(jwtManager *auth.JWTManager, logger *zap.Logger, baseUR
|
|||||||
return &ShortLinkService{
|
return &ShortLinkService{
|
||||||
jwtManager: jwtManager,
|
jwtManager: jwtManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
shortLinkRepo: repository.NewShortLinkRepository(),
|
||||||
|
patientRepo: repository.NewPatientRepository(),
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
defaultExpiryTime: time.Duration(defaultExpiryHours) * time.Hour,
|
defaultExpiryTime: time.Duration(defaultExpiryHours) * time.Hour,
|
||||||
maxAttempts: maxAttempts,
|
maxAttempts: maxAttempts,
|
||||||
shortLinks: make(map[string]*models.ShortLink),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,8 +125,12 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
|
|||||||
RemainingTries: s.maxAttempts,
|
RemainingTries: s.maxAttempts,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the short link (in production, this would be in a database)
|
// Store the short link in the database
|
||||||
s.shortLinks[token] = shortLink
|
err = s.shortLinkRepo.CreateShortLink(shortLink)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to store shortlink in database", zap.Error(err))
|
||||||
|
return nil, ErrCreationFailed
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -141,9 +147,14 @@ func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (
|
|||||||
// Get the token using the helper method that handles both field names
|
// Get the token using the helper method that handles both field names
|
||||||
token := req.GetToken()
|
token := req.GetToken()
|
||||||
|
|
||||||
// Find the short link
|
// Find the short link in the database
|
||||||
shortLink, exists := s.shortLinks[token]
|
shortLink, err := s.shortLinkRepo.GetShortLinkByToken(token)
|
||||||
if !exists {
|
if err != nil {
|
||||||
|
s.logger.Error("Error retrieving shortlink", zap.Error(err), zap.String("token", token))
|
||||||
|
return nil, ErrShortLinkNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if shortLink == nil {
|
||||||
return nil, ErrShortLinkNotFound
|
return nil, ErrShortLinkNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +179,13 @@ func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (
|
|||||||
if !isValidDOBFormat(dob) {
|
if !isValidDOBFormat(dob) {
|
||||||
// Decrement remaining tries on invalid format
|
// Decrement remaining tries on invalid format
|
||||||
shortLink.RemainingTries--
|
shortLink.RemainingTries--
|
||||||
|
|
||||||
|
// Update the shortlink in the database
|
||||||
|
err = s.shortLinkRepo.UpdateShortLink(shortLink)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,12 +195,25 @@ func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (
|
|||||||
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
|
||||||
|
err = s.shortLinkRepo.UpdateShortLink(shortLink)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to update shortlink tries", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
err = s.shortLinkRepo.UpdateShortLink(shortLink)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to update shortlink tries after successful validation", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
return shortLink, nil
|
return shortLink, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
internal/database/database.go
Normal file
43
internal/database/database.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB is the database instance
|
||||||
|
var DB *sqlx.DB
|
||||||
|
|
||||||
|
// Initialize creates and configures the database connection
|
||||||
|
func Initialize(dsn string, maxOpenConns, maxIdleConns int, connMaxLifetime time.Duration) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Connect to the database
|
||||||
|
DB, err = sqlx.Connect("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure connection pool
|
||||||
|
DB.SetMaxOpenConns(maxOpenConns)
|
||||||
|
DB.SetMaxIdleConns(maxIdleConns)
|
||||||
|
DB.SetConnMaxLifetime(connMaxLifetime)
|
||||||
|
|
||||||
|
// Check connection
|
||||||
|
if err := DB.Ping(); err != nil {
|
||||||
|
return fmt.Errorf("failed to ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the database connection
|
||||||
|
func Close() error {
|
||||||
|
if DB != nil {
|
||||||
|
return DB.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user