new file structure & koneksi ke DB
This commit is contained in:
@@ -4,10 +4,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"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/repository"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
|
||||
)
|
||||
|
||||
@@ -18,26 +20,37 @@ var (
|
||||
|
||||
// AuthService handles authentication operations
|
||||
type AuthService struct {
|
||||
jwtManager *auth.JWTManager
|
||||
// When you implement database connection, add a db client here
|
||||
// db *sqlx.DB
|
||||
jwtManager *auth.JWTManager
|
||||
userRepo *repository.UserRepository
|
||||
patientRepo *repository.PatientRepository
|
||||
}
|
||||
|
||||
// NewAuthService creates a new authentication service
|
||||
func NewAuthService(jwtManager *auth.JWTManager) *AuthService {
|
||||
return &AuthService{
|
||||
jwtManager: jwtManager,
|
||||
jwtManager: jwtManager,
|
||||
userRepo: repository.NewUserRepository(),
|
||||
patientRepo: repository.NewPatientRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a user and generates tokens
|
||||
func (s *AuthService) Login(email, password string) (*models.LoginResponse, error) {
|
||||
// Find user in mock data
|
||||
user := models.FindUserByCredentials(email, password)
|
||||
// Find user in database
|
||||
user, err := s.userRepo.GetUserByEmail(email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding user: %w", err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := CheckPassword(password, user.Password); err != nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Create token claims based on user role
|
||||
additionalClaims := make(map[string]interface{})
|
||||
var redirectURL string
|
||||
@@ -45,7 +58,11 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
||||
switch user.Role {
|
||||
case "patient":
|
||||
// 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 {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
@@ -53,15 +70,15 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
||||
// Set patient-specific claims
|
||||
additionalClaims["patient_id"] = patientData.PatientID
|
||||
additionalClaims["patient_name"] = patientData.PatientName
|
||||
additionalClaims["study_iuids"] = patientData.StudyIUIDs
|
||||
additionalClaims["study_iuids"] = patientData.StudyInstanceUIDs
|
||||
additionalClaims["accession_numbers"] = patientData.AccessionNumbers
|
||||
|
||||
// For redirectURL and home_url, use first study for simplicity
|
||||
if len(patientData.StudyIUIDs) > 0 {
|
||||
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyIUIDs[0])
|
||||
redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyIUIDs[0])
|
||||
if len(patientData.StudyInstanceUIDs) > 0 {
|
||||
additionalClaims["home_url"] = fmt.Sprintf("viewer?StudyInstanceUIDs=%s", patientData.StudyInstanceUIDs[0])
|
||||
redirectURL = fmt.Sprintf("/viewer?StudyInstanceUIDs=%s", patientData.StudyInstanceUIDs[0])
|
||||
} else {
|
||||
// Fallback for empty studies array (shouldn't happen)
|
||||
// Fallback for empty studies array
|
||||
additionalClaims["home_url"] = "/"
|
||||
redirectURL = "/"
|
||||
}
|
||||
@@ -79,7 +96,7 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
||||
|
||||
redirectURL = "/"
|
||||
|
||||
case "expertise_doctor":
|
||||
case "expertise_doctor", "admin":
|
||||
// Expertise doctors have full access
|
||||
additionalClaims["home_url"] = "/"
|
||||
additionalClaims["study_list"] = "enabled"
|
||||
@@ -98,6 +115,12 @@ func (s *AuthService) Login(email, password string) (*models.LoginResponse, erro
|
||||
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{
|
||||
AccessToken: accessToken,
|
||||
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
|
||||
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
|
||||
claims, err := s.jwtManager.ValidateToken(refreshToken)
|
||||
if err != nil {
|
||||
@@ -148,7 +192,13 @@ func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return "", err
|
||||
}
|
||||
@@ -161,6 +211,11 @@ func (s *AuthService) ValidateToken(token string) (*auth.CustomClaims, error) {
|
||||
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
|
||||
func HashPassword(password string) (string, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
@@ -174,17 +229,3 @@ func HashPassword(password string) (string, error) {
|
||||
func CheckPassword(password, hash string) error {
|
||||
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"
|
||||
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -38,14 +39,14 @@ var (
|
||||
|
||||
// ShortLinkService handles operations related to short links
|
||||
type ShortLinkService struct {
|
||||
jwtManager *auth.JWTManager
|
||||
logger *zap.Logger
|
||||
jwtManager *auth.JWTManager
|
||||
logger *zap.Logger
|
||||
shortLinkRepo *repository.ShortLinkRepository
|
||||
patientRepo *repository.PatientRepository
|
||||
// Configuration settings
|
||||
baseURL string
|
||||
defaultExpiryTime time.Duration
|
||||
maxAttempts int
|
||||
// Mock in-memory storage for now, would use a database in production
|
||||
shortLinks map[string]*models.ShortLink
|
||||
}
|
||||
|
||||
// NewShortLinkService creates a new short link service
|
||||
@@ -66,10 +67,11 @@ func NewShortLinkService(jwtManager *auth.JWTManager, logger *zap.Logger, baseUR
|
||||
return &ShortLinkService{
|
||||
jwtManager: jwtManager,
|
||||
logger: logger,
|
||||
shortLinkRepo: repository.NewShortLinkRepository(),
|
||||
patientRepo: repository.NewPatientRepository(),
|
||||
baseURL: baseURL,
|
||||
defaultExpiryTime: time.Duration(defaultExpiryHours) * time.Hour,
|
||||
maxAttempts: maxAttempts,
|
||||
shortLinks: make(map[string]*models.ShortLink),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +125,12 @@ func (s *ShortLinkService) GenerateShortLink(req *models.GenerateShortLinkReques
|
||||
RemainingTries: s.maxAttempts,
|
||||
}
|
||||
|
||||
// Store the short link (in production, this would be in a database)
|
||||
s.shortLinks[token] = shortLink
|
||||
// Store the short link in the database
|
||||
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
|
||||
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
|
||||
token := req.GetToken()
|
||||
|
||||
// Find the short link
|
||||
shortLink, exists := s.shortLinks[token]
|
||||
if !exists {
|
||||
// Find the short link in the database
|
||||
shortLink, err := s.shortLinkRepo.GetShortLinkByToken(token)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -168,6 +179,13 @@ func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (
|
||||
if !isValidDOBFormat(dob) {
|
||||
// Decrement remaining tries on invalid format
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -177,12 +195,25 @@ func (s *ShortLinkService) ValidateShortLink(req *models.ShortLinkAuthRequest) (
|
||||
if hashedDOB != shortLink.HashedDOB {
|
||||
// Decrement remaining tries on failed verification
|
||||
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
|
||||
}
|
||||
|
||||
// DOB verified, reset tries count as successful login
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user