add: login & token validation tapi belum connect ke DB
This commit is contained in:
@@ -4,39 +4,84 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/models"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/service"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication requests
|
||||
type AuthHandler struct {
|
||||
logger *zap.Logger
|
||||
logger *zap.Logger
|
||||
authService *service.AuthService
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new auth handler
|
||||
func NewAuthHandler(logger *zap.Logger) *AuthHandler {
|
||||
func NewAuthHandler(logger *zap.Logger, authService *service.AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
logger: logger,
|
||||
logger: logger,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// Login handles user login - placeholder for future implementation
|
||||
// Login handles user login
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"message": "Login functionality will be implemented in a future version",
|
||||
// Parse login request
|
||||
var req models.LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to parse login request", zap.Error(err))
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
response, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
h.logger.Warn("Login failed", zap.Error(err), zap.String("email", req.Email))
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return tokens and user info
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// Logout handles user logout - placeholder for future implementation
|
||||
// RefreshToken handles token refresh
|
||||
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse refresh token request
|
||||
var req models.RefreshRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("Failed to parse refresh token request", zap.Error(err))
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh token
|
||||
accessToken, err := h.authService.RefreshToken(req.RefreshToken)
|
||||
if err != nil {
|
||||
h.logger.Warn("Token refresh failed", zap.Error(err))
|
||||
http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return new access token
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(models.RefreshResponse{
|
||||
AccessToken: accessToken,
|
||||
})
|
||||
}
|
||||
|
||||
// Logout handles user logout
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"message": "Logout functionality will be implemented in a future version",
|
||||
}
|
||||
// In a real implementation, you would invalidate the refresh token
|
||||
// For now, just return a success message
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Successfully logged out",
|
||||
})
|
||||
}
|
||||
|
||||
194
internal/api/middleware/auth.go
Normal file
194
internal/api/middleware/auth.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/service"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
UserIDKey contextKey = "user_id"
|
||||
UserRoleKey contextKey = "user_role"
|
||||
UserEmailKey contextKey = "user_email"
|
||||
)
|
||||
|
||||
// WhitelistedEndpoints contains paths that can be accessed without authentication
|
||||
var WhitelistedEndpoints = []*regexp.Regexp{
|
||||
// Study by UID
|
||||
regexp.MustCompile(`^/dicomWeb/studies\?.*StudyInstanceUID=.+`),
|
||||
|
||||
// Frame endpoint
|
||||
regexp.MustCompile(`^/dicomWeb/studies/[^/]+/series/[^/]+/instances/[^/]+/frames/\d+$`),
|
||||
}
|
||||
|
||||
// Auth middleware authenticates requests using JWT tokens
|
||||
func Auth(authService *service.AuthService, logger *zap.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request path is whitelisted
|
||||
path := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
path = path + "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
for _, pattern := range WhitelistedEndpoints {
|
||||
if pattern.MatchString(path) {
|
||||
// Path is whitelisted, skip authentication
|
||||
logger.Debug("Skipping authentication for whitelisted path", zap.String("path", path))
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
logger.Warn("Missing Authorization header")
|
||||
respondWithError(w, http.StatusUnauthorized, "missing authorization header")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract token from Bearer token
|
||||
bearerToken := strings.Split(authHeader, " ")
|
||||
if len(bearerToken) != 2 || strings.ToLower(bearerToken[0]) != "bearer" {
|
||||
logger.Warn("Invalid Authorization header format")
|
||||
respondWithError(w, http.StatusUnauthorized, "invalid authorization format")
|
||||
return
|
||||
}
|
||||
|
||||
token := bearerToken[1]
|
||||
|
||||
// Validate token
|
||||
claims, err := authService.ValidateToken(token)
|
||||
if err != nil {
|
||||
logger.Warn("Invalid or expired token", zap.Error(err))
|
||||
respondWithError(w, http.StatusUnauthorized, "invalid or expired token")
|
||||
return
|
||||
}
|
||||
|
||||
// Check token type
|
||||
if claims.TokenType != "access" {
|
||||
logger.Warn("Invalid token type", zap.String("tokenType", claims.TokenType))
|
||||
respondWithError(w, http.StatusUnauthorized, "invalid token type")
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Log user info
|
||||
logger.Info("Authenticated user", zap.String("userID", claims.UserID), zap.String("role", claims.Role), zap.String("email", claims.Email))
|
||||
|
||||
// Continue with the request
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RoleRequired middleware checks if user has the required role
|
||||
func RoleRequired(roles ...string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request path is whitelisted first
|
||||
path := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
path = path + "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
for _, pattern := range WhitelistedEndpoints {
|
||||
if pattern.MatchString(path) {
|
||||
// Path is whitelisted, skip role check
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get user role from context
|
||||
userRole, ok := r.Context().Value(UserRoleKey).(string)
|
||||
if !ok {
|
||||
respondWithError(w, http.StatusUnauthorized, "user context not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user has one of the required roles
|
||||
hasRole := false
|
||||
for _, role := range roles {
|
||||
if userRole == role {
|
||||
hasRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRole {
|
||||
respondWithError(w, http.StatusForbidden, "insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Continue with the request
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// PatientViewRestriction middleware restricts patients to view only their studies
|
||||
func PatientViewRestriction(logger *zap.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request path is whitelisted first
|
||||
path := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
path = path + "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
for _, pattern := range WhitelistedEndpoints {
|
||||
if pattern.MatchString(path) {
|
||||
// Path is whitelisted, skip restrictions
|
||||
logger.Debug("Skipping patient restrictions for whitelisted path", zap.String("path", path))
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get user role from context
|
||||
userRole, ok := r.Context().Value(UserRoleKey).(string)
|
||||
if !ok {
|
||||
respondWithError(w, http.StatusUnauthorized, "user context not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Only apply restrictions to patients
|
||||
if userRole != "patient" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Logic to restrict patients to only access their assigned studies
|
||||
// For now, we're just letting the request through, but in a real
|
||||
// implementation, you would check the study ID against the patient's
|
||||
// assigned studies.
|
||||
|
||||
// TODO: Check if the requested study is assigned to the patient
|
||||
// This would likely involve parsing the URL path to extract study ID
|
||||
// and checking it against a database of patient assignments
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to respond with JSON error
|
||||
func respondWithError(w http.ResponseWriter, code int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": message})
|
||||
}
|
||||
61
internal/api/models/user.go
Normal file
61
internal/api/models/user.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package models
|
||||
|
||||
// User represents a system user
|
||||
type User struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
Email string `db:"email" json:"email"`
|
||||
Password string `db:"password" json:"-"` // Never expose password in JSON
|
||||
Role string `db:"role" json:"role"`
|
||||
Name string `db:"name" json:"name"`
|
||||
CreatedAt string `db:"created_at" json:"created_at"`
|
||||
UpdatedAt string `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// RefreshToken represents a refresh token stored in the database
|
||||
type RefreshToken struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
UserID string `db:"user_id" json:"user_id"`
|
||||
Token string `db:"token" json:"token"`
|
||||
ExpiresAt string `db:"expires_at" json:"expires_at"`
|
||||
IsRevoked bool `db:"is_revoked" json:"is_revoked"`
|
||||
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"`
|
||||
AccessionNumber string `json:"accession_number"`
|
||||
StudyInstanceUID string `json:"study_instance_uid"`
|
||||
}
|
||||
|
||||
// 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
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// LoginResponse is the response sent after successful login
|
||||
type LoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
User *User `json:"user"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
// RefreshRequest represents the refresh token request
|
||||
type RefreshRequest struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
// RefreshResponse is the response for a token refresh
|
||||
type RefreshResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
@@ -2,12 +2,15 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/config"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/handlers"
|
||||
apiMiddleware "devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/middleware"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/api/service"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/auth"
|
||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/proxy"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
@@ -18,29 +21,23 @@ import (
|
||||
func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Built-in Chi middleware
|
||||
// Base middleware
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.StripSlashes)
|
||||
|
||||
// Custom middleware
|
||||
r.Use(apiMiddleware.Logger(logger))
|
||||
|
||||
// CORS middleware
|
||||
// CORS configuration
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: cfg.AllowedOrigins,
|
||||
AllowedOrigins: []string{"*"}, // In production, restrict this to your frontend domains
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
|
||||
// Setup health check
|
||||
r.Get("/health", handlers.HealthCheck)
|
||||
|
||||
// Initialize Google auth client
|
||||
// Initialize Google auth client for proxy
|
||||
googleAuth, err := auth.NewGoogleClient(cfg.Google.CredentialsPath)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to initialize Google auth client", zap.Error(err))
|
||||
@@ -49,23 +46,67 @@ func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||
// Initialize Healthcare API client
|
||||
healthcareClient := proxy.NewClient(googleAuth, cfg.Google)
|
||||
|
||||
// DICOM Web routes - simplified approach
|
||||
r.Route("/dicomWeb", func(r chi.Router) {
|
||||
// Add audit logging middleware to DICOM routes
|
||||
r.Use(apiMiddleware.AuditLog(logger))
|
||||
// Initialize JWT auth service
|
||||
jwtSecret := cfg.Auth.JWTSecret
|
||||
if jwtSecret == "" {
|
||||
jwtSecret = "vQ6PQqUyh7pBNOytClgN+Nw1XBq7F8Qo6VP3VwIqvHY=" // Default from your example, should be set in config
|
||||
}
|
||||
|
||||
// Create single handler for all DICOM requests
|
||||
dicomHandler := handlers.NewDicomHandler(healthcareClient, logger)
|
||||
// Convert config values to time.Duration
|
||||
accessExpiry := time.Duration(cfg.Auth.AccessTokenExpiry) * time.Minute
|
||||
refreshExpiry := time.Duration(cfg.Auth.RefreshTokenExpiry) * time.Hour
|
||||
|
||||
// Catch all routes under /dicomWeb and forward them
|
||||
r.HandleFunc("/*", dicomHandler.ForwardRequest)
|
||||
// Create JWT manager with config values
|
||||
jwtManager := auth.NewJWTManager(jwtSecret, accessExpiry, refreshExpiry)
|
||||
authService := service.NewAuthService(jwtManager)
|
||||
|
||||
// Public routes that don't require authentication
|
||||
r.Group(func(r chi.Router) {
|
||||
// Health check
|
||||
r.Get("/health", handlers.HealthCheck)
|
||||
|
||||
// Authentication endpoints
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
authHandler := handlers.NewAuthHandler(logger, authService)
|
||||
r.Post("/login", authHandler.Login)
|
||||
r.Post("/refresh", authHandler.RefreshToken)
|
||||
r.Post("/logout", authHandler.Logout)
|
||||
})
|
||||
})
|
||||
|
||||
// Future auth routes for doctors
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
authHandler := handlers.NewAuthHandler(logger)
|
||||
r.Post("/login", authHandler.Login)
|
||||
r.Post("/logout", authHandler.Logout)
|
||||
// Protected routes that require authentication
|
||||
r.Group(func(r chi.Router) {
|
||||
// Apply authentication middleware
|
||||
r.Use(apiMiddleware.Auth(authService, logger))
|
||||
|
||||
// DICOM Web routes
|
||||
r.Route("/dicomWeb", func(r chi.Router) {
|
||||
// Add audit logging middleware to DICOM routes
|
||||
r.Use(apiMiddleware.AuditLog(logger))
|
||||
|
||||
// Add patient view restriction for patient role
|
||||
r.Use(apiMiddleware.PatientViewRestriction(logger))
|
||||
|
||||
// Create handler for all DICOM requests
|
||||
dicomHandler := handlers.NewDicomHandler(healthcareClient, logger)
|
||||
|
||||
// Common routes for studies with role-specific handling
|
||||
r.Route("/studies", func(r chi.Router) {
|
||||
// StudyInstanceUID parameter routes - accessible by all roles
|
||||
r.Get("/{studyInstanceUID}", dicomHandler.ForwardRequest) // Study details
|
||||
r.Get("/{studyInstanceUID}/series", dicomHandler.ForwardRequest) // Series list for study
|
||||
|
||||
// Deep hierarchy routes - accessible by patients and all doctors
|
||||
r.Get("/{studyInstanceUID}/series/{seriesUID}/metadata", dicomHandler.ForwardRequest)
|
||||
r.Get("/{studyInstanceUID}/series/{seriesUID}/instances/{instanceUID}/frames/{frame}", dicomHandler.ForwardRequest)
|
||||
|
||||
// Query routes - accessible by all roles
|
||||
r.Get("/", dicomHandler.ForwardRequest) // Study list with filters
|
||||
})
|
||||
|
||||
// Expertise doctors have full access to all DICOM endpoints
|
||||
r.With(apiMiddleware.RoleRequired("expertise_doctor")).HandleFunc("/*", dicomHandler.ForwardRequest)
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
|
||||
217
internal/api/service/auth_service.go
Normal file
217
internal/api/service/auth_service.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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/auth"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
)
|
||||
|
||||
// AuthService handles authentication operations
|
||||
type AuthService struct {
|
||||
jwtManager *auth.JWTManager
|
||||
// When you implement database connection, add a db client here
|
||||
// db *sqlx.DB
|
||||
}
|
||||
|
||||
// NewAuthService creates a new authentication service
|
||||
func NewAuthService(jwtManager *auth.JWTManager) *AuthService {
|
||||
return &AuthService{
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates a user and generates tokens
|
||||
func (s *AuthService) Login(email, password string) (*models.LoginResponse, error) {
|
||||
// For now, use hardcoded credentials
|
||||
// TODO: In a real implementation, you would query the database
|
||||
if email == "admin" && password == "admin" {
|
||||
// Create a dummy user
|
||||
user := &models.User{
|
||||
ID: "1",
|
||||
Email: "admin",
|
||||
Role: "expertise_doctor",
|
||||
Name: "Admin User",
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, user.Email, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: In a real implementation, you would store the refresh token in the database
|
||||
// For example:
|
||||
// s.storeRefreshToken(user.ID, refreshToken)
|
||||
|
||||
// Determine redirect URL based on role
|
||||
redirectURL := "/viewer"
|
||||
if user.Role == "ref_doctor" || user.Role == "expertise_doctor" {
|
||||
redirectURL = "/studylist"
|
||||
}
|
||||
|
||||
return &models.LoginResponse{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
User: user,
|
||||
RedirectURL: redirectURL,
|
||||
}, nil
|
||||
} else if email == "patient" && password == "patient" {
|
||||
// Create a patient user
|
||||
user := &models.User{
|
||||
ID: "2",
|
||||
Email: "patient",
|
||||
Role: "patient",
|
||||
Name: "Patient User",
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Generate tokens with patient-specific claims
|
||||
accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, user.Email, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.LoginResponse{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
User: user,
|
||||
RedirectURL: "/viewer",
|
||||
}, nil
|
||||
} else if email == "doctor" && password == "doctor" {
|
||||
// Create a referring doctor user
|
||||
user := &models.User{
|
||||
ID: "3",
|
||||
Email: "doctor",
|
||||
Role: "ref_doctor",
|
||||
Name: "Doctor User",
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID, user.Email, user.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.LoginResponse{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
User: user,
|
||||
RedirectURL: "/studylist",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// RefreshToken generates a new access token using a refresh token
|
||||
func (s *AuthService) RefreshToken(refreshToken string) (string, error) {
|
||||
// Validate the refresh token
|
||||
claims, err := s.jwtManager.ValidateToken(refreshToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if token is a refresh token
|
||||
if claims.TokenType != "refresh" {
|
||||
return "", errors.New("invalid token type")
|
||||
}
|
||||
|
||||
// TODO: In a real implementation, you would check if the token is in the database and not revoked
|
||||
// Here we just generate a new access token
|
||||
accessToken, err := s.jwtManager.GenerateAccessToken(claims.UserID, claims.Email, claims.Role)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates a token and returns the claims
|
||||
func (s *AuthService) ValidateToken(token string) (*auth.CustomClaims, error) {
|
||||
return s.jwtManager.ValidateToken(token)
|
||||
}
|
||||
|
||||
// HashPassword hashes a password using bcrypt
|
||||
func HashPassword(password string) (string, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
// CheckPassword compares a password with a hash
|
||||
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
|
||||
// For example:
|
||||
/*
|
||||
refreshToken := &models.RefreshToken{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Token: token,
|
||||
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Format(time.RFC3339),
|
||||
IsRevoked: false,
|
||||
CreatedAt: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
_, err := s.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
|
||||
*/
|
||||
|
||||
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
|
||||
// For example:
|
||||
/*
|
||||
_, err := s.db.Exec(
|
||||
"UPDATE refresh_tokens SET is_revoked = true WHERE token = ?",
|
||||
token,
|
||||
)
|
||||
return err
|
||||
*/
|
||||
|
||||
return nil
|
||||
}
|
||||
130
internal/api/service/db_repository.go
Normal file
130
internal/api/service/db_repository.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user