add: login & token validation tapi belum connect ke DB
This commit is contained in:
41
README.md
41
README.md
@@ -28,6 +28,47 @@ Berikut adalah cara clone project ini:
|
|||||||
3. **Pemrosesan Google Cloud**: Healthcare API memproses permintaan DICOM.
|
3. **Pemrosesan Google Cloud**: Healthcare API memproses permintaan DICOM.
|
||||||
4. **Penanganan Respons**: Proxy meneruskan respons kembali ke klien.
|
4. **Penanganan Respons**: Proxy meneruskan respons kembali ke klien.
|
||||||
|
|
||||||
|
## 2b. Arsitektur
|
||||||
|
```txt
|
||||||
|
go-ohif-proxy/
|
||||||
|
│
|
||||||
|
├── cmd/
|
||||||
|
│ └── server/ # Application entry point
|
||||||
|
│ └── main.go # Main server initialization
|
||||||
|
│
|
||||||
|
├── config/ # Configuration management
|
||||||
|
│ ├── config.go # Configuration loader
|
||||||
|
│ └── config.yaml # Application configuration
|
||||||
|
│
|
||||||
|
├── credentials/ # Authentication credentials
|
||||||
|
│ └── service-account.json # Google Cloud service account
|
||||||
|
│
|
||||||
|
├── internal/
|
||||||
|
│ ├── api/ # API endpoints
|
||||||
|
│ │ ├── handler.go # HTTP request handlers
|
||||||
|
│ │ ├── middleware.go # Request middleware
|
||||||
|
│ │ └── routes.go # API route definitions
|
||||||
|
│ │
|
||||||
|
│ ├── auth/ # Authentication services
|
||||||
|
│ │ └── google.go # Google Cloud authentication
|
||||||
|
│ │
|
||||||
|
│ ├── proxy/ # Proxy service
|
||||||
|
│ │ └── dicomweb.go # DICOMweb request handling
|
||||||
|
│ │
|
||||||
|
│ └── utils/ # Utility functions
|
||||||
|
│ ├── http.go # HTTP utilities
|
||||||
|
│ └── logger.go # Logging functionality
|
||||||
|
│
|
||||||
|
├── test/ # Testing resources
|
||||||
|
│ └── http/ # HTTP test requests
|
||||||
|
│
|
||||||
|
├── Dockerfile # Container definition
|
||||||
|
├── docker-compose.yml # Multi-container orchestration
|
||||||
|
├── go.mod # Go module definition
|
||||||
|
├── go.sum # Module dependency checksums
|
||||||
|
└── README.md # Project documentation
|
||||||
|
```
|
||||||
|
|
||||||
## 3. Instalasi dan Penggunaan
|
## 3. Instalasi dan Penggunaan
|
||||||
|
|
||||||
### Prasyarat
|
### Prasyarat
|
||||||
|
|||||||
@@ -26,6 +26,21 @@ type Config struct {
|
|||||||
CredentialsPath string `mapstructure:"credentials_path"`
|
CredentialsPath string `mapstructure:"credentials_path"`
|
||||||
} `mapstructure:"google"`
|
} `mapstructure:"google"`
|
||||||
|
|
||||||
|
Auth struct {
|
||||||
|
JWTSecret string `mapstructure:"jwt_secret"`
|
||||||
|
AccessTokenExpiry int `mapstructure:"access_token_expiry"` // in minutes
|
||||||
|
RefreshTokenExpiry int `mapstructure:"refresh_token_expiry"` // in hours
|
||||||
|
EnableDatabaseAuth bool `mapstructure:"enable_database_auth"`
|
||||||
|
} `mapstructure:"auth"`
|
||||||
|
|
||||||
|
Database struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
} `mapstructure:"database"`
|
||||||
|
|
||||||
AllowedOrigins []string `mapstructure:"allowed_origins"`
|
AllowedOrigins []string `mapstructure:"allowed_origins"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,18 @@ google:
|
|||||||
dicom_store: "store-1" # Your DICOM store name
|
dicom_store: "store-1" # Your DICOM store name
|
||||||
credentials_path: "./credentials/service-account.json"
|
credentials_path: "./credentials/service-account.json"
|
||||||
|
|
||||||
|
auth:
|
||||||
|
jwt_secret: "vQ6PQqUyh7pBNOytClgN+Nw1XBq7F8Qo6VP3VwIqvHY=" # Change this in production!
|
||||||
|
access_token_expiry: 1440 # minutes (24 hours)
|
||||||
|
refresh_token_expiry: 168 # hours (7 days)
|
||||||
|
enable_database_auth: false # Set to true when ready to use database
|
||||||
|
|
||||||
|
database:
|
||||||
|
host: "localhost"
|
||||||
|
port: 3306
|
||||||
|
user: "dbuser"
|
||||||
|
password: "dbpassword" # Consider using environment variables for sensitive data
|
||||||
|
name: "ohif_proxy"
|
||||||
|
|
||||||
allowed_origins:
|
allowed_origins:
|
||||||
- "*" # For development; restrict this in production
|
- "*" # For development; restrict this in production
|
||||||
12
go.mod
12
go.mod
@@ -1,12 +1,19 @@
|
|||||||
module devone.aplikasi.web.id/gitea/mario/go-ohif-proxy
|
module devone.aplikasi.web.id/gitea/mario/go-ohif-proxy
|
||||||
|
|
||||||
go 1.19
|
go 1.21.0
|
||||||
|
|
||||||
|
toolchain go1.23.8
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.2.1
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
|
github.com/go-sql-driver/mysql v1.9.2
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
|
github.com/google/uuid v1.4.0
|
||||||
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/spf13/viper v1.17.0
|
github.com/spf13/viper v1.17.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/oauth2 v0.13.0
|
golang.org/x/oauth2 v0.13.0
|
||||||
google.golang.org/api v0.149.0
|
google.golang.org/api v0.149.0
|
||||||
)
|
)
|
||||||
@@ -14,11 +21,11 @@ require (
|
|||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.23.1 // indirect
|
cloud.google.com/go/compute v1.23.1 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/s2a-go v0.1.7 // indirect
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
github.com/google/uuid v1.4.0 // indirect
|
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
@@ -34,7 +41,6 @@ require (
|
|||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/crypto v0.14.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/net v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.13.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
|||||||
25
go.sum
25
go.sum
@@ -40,6 +40,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
|||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@@ -53,6 +55,7 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
@@ -60,6 +63,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||||
|
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
@@ -69,6 +73,11 @@ github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vz
|
|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -113,6 +122,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
@@ -145,17 +155,25 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||||
@@ -164,9 +182,11 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
|
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
|
||||||
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
@@ -207,6 +227,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
|||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
@@ -312,6 +333,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||||
|
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -481,7 +503,9 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
|
|||||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA=
|
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA=
|
||||||
|
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k=
|
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
@@ -519,6 +543,7 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
|||||||
@@ -4,39 +4,84 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"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"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthHandler handles authentication requests
|
// AuthHandler handles authentication requests
|
||||||
type AuthHandler struct {
|
type AuthHandler struct {
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
authService *service.AuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthHandler creates a new auth handler
|
// NewAuthHandler creates a new auth handler
|
||||||
func NewAuthHandler(logger *zap.Logger) *AuthHandler {
|
func NewAuthHandler(logger *zap.Logger, authService *service.AuthService) *AuthHandler {
|
||||||
return &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) {
|
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
response := map[string]string{
|
// Parse login request
|
||||||
"message": "Login functionality will be implemented in a future version",
|
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.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(response)
|
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) {
|
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||||
response := map[string]string{
|
// In a real implementation, you would invalidate the refresh token
|
||||||
"message": "Logout functionality will be implemented in a future version",
|
// For now, just return a success message
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
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 (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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/handlers"
|
"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"
|
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/auth"
|
||||||
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/proxy"
|
"devone.aplikasi.web.id/gitea/mario/go-ohif-proxy/internal/proxy"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
@@ -18,29 +21,23 @@ import (
|
|||||||
func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
func SetupRouter(cfg *config.Config, logger *zap.Logger) http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
// Built-in Chi middleware
|
// Base middleware
|
||||||
r.Use(middleware.RequestID)
|
r.Use(middleware.RequestID)
|
||||||
r.Use(middleware.RealIP)
|
r.Use(middleware.RealIP)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
r.Use(middleware.StripSlashes)
|
|
||||||
|
|
||||||
// Custom middleware
|
|
||||||
r.Use(apiMiddleware.Logger(logger))
|
r.Use(apiMiddleware.Logger(logger))
|
||||||
|
|
||||||
// CORS middleware
|
// CORS configuration
|
||||||
r.Use(cors.Handler(cors.Options{
|
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"},
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||||
ExposedHeaders: []string{"Link"},
|
ExposedHeaders: []string{"Link"},
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
MaxAge: 300,
|
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Setup health check
|
// Initialize Google auth client for proxy
|
||||||
r.Get("/health", handlers.HealthCheck)
|
|
||||||
|
|
||||||
// Initialize Google auth client
|
|
||||||
googleAuth, err := auth.NewGoogleClient(cfg.Google.CredentialsPath)
|
googleAuth, err := auth.NewGoogleClient(cfg.Google.CredentialsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("Failed to initialize Google auth client", zap.Error(err))
|
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
|
// Initialize Healthcare API client
|
||||||
healthcareClient := proxy.NewClient(googleAuth, cfg.Google)
|
healthcareClient := proxy.NewClient(googleAuth, cfg.Google)
|
||||||
|
|
||||||
// DICOM Web routes - simplified approach
|
// Initialize JWT auth service
|
||||||
r.Route("/dicomWeb", func(r chi.Router) {
|
jwtSecret := cfg.Auth.JWTSecret
|
||||||
// Add audit logging middleware to DICOM routes
|
if jwtSecret == "" {
|
||||||
r.Use(apiMiddleware.AuditLog(logger))
|
jwtSecret = "vQ6PQqUyh7pBNOytClgN+Nw1XBq7F8Qo6VP3VwIqvHY=" // Default from your example, should be set in config
|
||||||
|
}
|
||||||
|
|
||||||
// Create single handler for all DICOM requests
|
// Convert config values to time.Duration
|
||||||
dicomHandler := handlers.NewDicomHandler(healthcareClient, logger)
|
accessExpiry := time.Duration(cfg.Auth.AccessTokenExpiry) * time.Minute
|
||||||
|
refreshExpiry := time.Duration(cfg.Auth.RefreshTokenExpiry) * time.Hour
|
||||||
|
|
||||||
// Catch all routes under /dicomWeb and forward them
|
// Create JWT manager with config values
|
||||||
r.HandleFunc("/*", dicomHandler.ForwardRequest)
|
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
|
// Protected routes that require authentication
|
||||||
r.Route("/auth", func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
authHandler := handlers.NewAuthHandler(logger)
|
// Apply authentication middleware
|
||||||
r.Post("/login", authHandler.Login)
|
r.Use(apiMiddleware.Auth(authService, logger))
|
||||||
r.Post("/logout", authHandler.Logout)
|
|
||||||
|
// 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
|
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()
|
||||||
|
}
|
||||||
101
internal/auth/jwt.go
Normal file
101
internal/auth/jwt.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidToken = errors.New("invalid token")
|
||||||
|
ErrExpiredToken = errors.New("token has expired")
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWTManager handles JWT token operations
|
||||||
|
type JWTManager struct {
|
||||||
|
secretKey string
|
||||||
|
accessExpiry time.Duration
|
||||||
|
refreshExpiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJWTManager creates a new JWT manager
|
||||||
|
func NewJWTManager(secretKey string, accessExpiry, refreshExpiry time.Duration) *JWTManager {
|
||||||
|
return &JWTManager{
|
||||||
|
secretKey: secretKey,
|
||||||
|
accessExpiry: accessExpiry,
|
||||||
|
refreshExpiry: refreshExpiry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomClaims contains the claims we want in our tokens
|
||||||
|
type CustomClaims struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
TokenType string `json:"token_type"` // access or refresh
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAccessToken creates a new access token
|
||||||
|
func (m *JWTManager) GenerateAccessToken(userID, email, role string) (string, error) {
|
||||||
|
claims := CustomClaims{
|
||||||
|
UserID: userID,
|
||||||
|
Email: email,
|
||||||
|
Role: role,
|
||||||
|
TokenType: "access",
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.accessExpiry)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString([]byte(m.secretKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRefreshToken creates a new refresh token
|
||||||
|
func (m *JWTManager) GenerateRefreshToken(userID, email, role string) (string, error) {
|
||||||
|
claims := CustomClaims{
|
||||||
|
UserID: userID,
|
||||||
|
Email: email,
|
||||||
|
Role: role,
|
||||||
|
TokenType: "refresh",
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.refreshExpiry)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString([]byte(m.secretKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken validates a token and returns the claims
|
||||||
|
func (m *JWTManager) ValidateToken(tokenString string) (*CustomClaims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(
|
||||||
|
tokenString,
|
||||||
|
&CustomClaims{},
|
||||||
|
func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return []byte(m.secretKey), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||||
|
return nil, ErrExpiredToken
|
||||||
|
}
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*CustomClaims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
BIN
ohif-proxy
BIN
ohif-proxy
Binary file not shown.
BIN
ohif-proxy-old
Executable file
BIN
ohif-proxy-old
Executable file
Binary file not shown.
57
test/http/ohif-flow.http
Normal file
57
test/http/ohif-flow.http
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMiIsImVtYWlsIjoicGF0aWVudCIsInJvbGUiOiJwYXRpZW50IiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImV4cCI6MTc0NjUwNDUzMCwiaWF0IjoxNzQ2NDE4MTMwfQ.AvSBHvy3y22Pa4M8MZS9u00fiBtHzcS_WbxukxsBcj4
|
||||||
|
@token_exp_doctor = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsImVtYWlsIjoiYWRtaW4iLCJyb2xlIjoiZXhwZXJ0aXNlX2RvY3RvciIsInRva2VuX3R5cGUiOiJhY2Nlc3MiLCJleHAiOjE3NDY1MDQ1MTYsImlhdCI6MTc0NjQxODExNn0.vlDrns1oPFXHE5--TmWqwzvzxnfcCPcV2UW8_4GwDwE
|
||||||
|
@baseUrl = http://localhost:5555
|
||||||
|
|
||||||
|
### Login Patient
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "patient",
|
||||||
|
"password": "patient"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Login Admin / Exp_doctor (ALL ACCESS)
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "admin",
|
||||||
|
"password": "admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
### * === PATIENT === *
|
||||||
|
|
||||||
|
# Destination URL / OHIF Viewer URL: http://152.42.173.210:3000/viewer?StudyInstanceUIDs=1.2.826.0.1.3680043.9.7307.1.202503196393.01
|
||||||
|
|
||||||
|
### Study where StudyIUID
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.202503196393.01
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
### Series List
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.202503196393.01/series?includefield=00080021,00080031,0008103E,00200011
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
### Semua Study dari Patient ID
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies?00100020=MR00000359&limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
### Metadata
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.202503196393.01/series/1.2.826.0.1.3680043.2.1545.1.2.1.7.20250319.100353.734.4/metadata
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
### Modality and Study Description untuk Study Instance UID = ...
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.202503196393.01
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
### DICOM Frames
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.202503196393.01/series/1.2.826.0.1.3680043.2.1545.1.2.1.7.20250319.100353.734.4/instances/1.2.826.0.1.3680043.2.1545.1.2.1.7.20250319.100353.1.5/frames/1
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
### Modality and Study Description untuk semua studies pada PatientID = ...
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies?00100020=MR00000359&limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
|
### Accession, Deskripsi Studi, Umur, dan format nama pasien where StudyInstanceUID = ...
|
||||||
|
GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=false&includefield=00080050,00081030,00101010,0010004&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.202503196393.01
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
@@ -1,18 +1,37 @@
|
|||||||
### Local OHIF Proxy Test File
|
### Local OHIF Proxy Test File
|
||||||
# @baseUrl = http://localhost:5555
|
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMiIsImVtYWlsIjoicGF0aWVudCIsInJvbGUiOiJwYXRpZW50IiwidG9rZW5fdHlwZSI6ImFjY2VzcyIsImV4cCI6MTc0NjM2NDczMiwiaWF0IjoxNzQ2MzYyOTMyfQ.4IGGV77jnewQVXOCuFWmcx4X7EMMxx341j6DeNKYcFY
|
||||||
# @baseUrl = http://devone.aplikasi.web.id:5555
|
@baseUrl = http://localhost:5555
|
||||||
@baseUrl = http://152.42.173.210:5555
|
|
||||||
|
|
||||||
|
# @baseUrl = http://devone.aplikasi.web.id:5555
|
||||||
|
# @baseUrl = http://152.42.173.210:5555
|
||||||
|
|
||||||
### 1. Health Check
|
### 1. Health Check
|
||||||
# Verifies that the proxy server is running
|
# Verifies that the proxy server is running
|
||||||
GET {{baseUrl}}/health
|
GET {{baseUrl}}/health
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
|
|
||||||
|
### Login Success
|
||||||
|
POST {{baseUrl}}/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "patient",
|
||||||
|
"password": "patient"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Refresh TOken
|
||||||
|
POST {{baseUrl}}/auth/refresh
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"refresh"
|
||||||
|
}
|
||||||
|
|
||||||
### 2. QIDO-RS: Search for Studies
|
### 2. QIDO-RS: Search for Studies
|
||||||
# Returns all studies (should return a list of DICOM studies if any exist)
|
# Returns all studies (should return a list of DICOM studies if any exist)
|
||||||
GET {{baseUrl}}/dicomWeb/studies
|
GET {{baseUrl}}/dicomWeb/studies
|
||||||
Accept: application/dicom+json
|
Accept: application/dicom+json
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
### 3. QIDO-RS: Search for Studies with Patient Name
|
### 3. QIDO-RS: Search for Studies with Patient Name
|
||||||
# Returns studies matching patient name (replace SMITH with a name in your dataset)
|
# Returns studies matching patient name (replace SMITH with a name in your dataset)
|
||||||
@@ -49,6 +68,7 @@ Accept: application/dicom+json
|
|||||||
# Replace STUDY_INSTANCE_UID, SERIES_INSTANCE_UID and SOP_INSTANCE_UID with actual values
|
# Replace STUDY_INSTANCE_UID, SERIES_INSTANCE_UID and SOP_INSTANCE_UID with actual values
|
||||||
GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.202503196393.01/series/1.2.826.0.1.3680043.2.1545.1.2.1.7.20250319.100353.734.4/metadata
|
GET {{baseUrl}}/dicomWeb/studies/1.2.826.0.1.3680043.9.7307.1.202503196393.01/series/1.2.826.0.1.3680043.2.1545.1.2.1.7.20250319.100353.734.4/metadata
|
||||||
Accept: */*
|
Accept: */*
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
|
||||||
### 10. WADO-RS: Retrieve Instance
|
### 10. WADO-RS: Retrieve Instance
|
||||||
# Replace STUDY_INSTANCE_UID, SERIES_INSTANCE_UID and SOP_INSTANCE_UID with actual values
|
# Replace STUDY_INSTANCE_UID, SERIES_INSTANCE_UID and SOP_INSTANCE_UID with actual values
|
||||||
@@ -70,3 +90,5 @@ Accept: image/jpeg
|
|||||||
|
|
||||||
####
|
####
|
||||||
GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.202503196393.01
|
GET {{baseUrl}}/dicomWeb/studies?limit=101&offset=0&fuzzymatching=true&includefield=00081030,00080060&StudyInstanceUID=1.2.826.0.1.3680043.9.7307.1.202503196393.01
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user