diff --git a/.env.example b/.env.example index 091a731..2c9122a 100644 --- a/.env.example +++ b/.env.example @@ -35,5 +35,6 @@ DB_ANTRIAN_SSLMODE=disable KEYCLOAK_BASE_URL=https://keycloak.example.com KEYCLOAK_REALM=myrealm KEYCLOAK_AUDIENCE=my-client-id +KEYCLOAK_SECRET_KEY=super-secret KEYCLOAK_ISSUER=https://keycloak.example.com/realms/myrealm KEYCLOAK_IS_ENABLE=false \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index 4073d99..790a663 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -504,6 +504,37 @@ const docTemplate = `{ } } }, + "/keycloak/refresh-token": { + "post": { + "tags": [ + "Keycloak" + ], + "summary": "Requesting new token to keycloak using refresh token", + "parameters": [ + { + "type": "string", + "description": "Valid Refresh Token", + "name": "refresh_token", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/shared.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/shared.BaseErrorResponse" + } + } + } + } + }, "/reference/diagnosa/": { "get": { "tags": [ @@ -875,8 +906,7 @@ const docTemplate = `{ "jenisKelamin", "namaPasien", "noKtp", - "noRekamMedis", - "nomorTelepon" + "noRekamMedis" ], "properties": { "alamat": { @@ -900,7 +930,6 @@ const docTemplate = `{ }, "nomorTelepon": { "type": "array", - "minItems": 1, "items": { "type": "string" } diff --git a/docs/swagger.json b/docs/swagger.json index 8359078..d1cc71b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -498,6 +498,37 @@ } } }, + "/keycloak/refresh-token": { + "post": { + "tags": [ + "Keycloak" + ], + "summary": "Requesting new token to keycloak using refresh token", + "parameters": [ + { + "type": "string", + "description": "Valid Refresh Token", + "name": "refresh_token", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/shared.BaseResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/shared.BaseErrorResponse" + } + } + } + } + }, "/reference/diagnosa/": { "get": { "tags": [ @@ -869,8 +900,7 @@ "jenisKelamin", "namaPasien", "noKtp", - "noRekamMedis", - "nomorTelepon" + "noRekamMedis" ], "properties": { "alamat": { @@ -894,7 +924,6 @@ }, "nomorTelepon": { "type": "array", - "minItems": 1, "items": { "type": "string" } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index eb33486..a864c0f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -94,7 +94,6 @@ definitions: nomorTelepon: items: type: string - minItems: 1 type: array tanggalLahir: type: string @@ -105,7 +104,6 @@ definitions: - namaPasien - noKtp - noRekamMedis - - nomorTelepon type: object antrianoperasi.PasienOperasi: properties: @@ -747,6 +745,26 @@ paths: summary: Get Table Antrian per Sub Spesialis tags: - Dashboard + /keycloak/refresh-token: + post: + parameters: + - description: Valid Refresh Token + in: query + name: refresh_token + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/shared.BaseResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/shared.BaseErrorResponse' + summary: Requesting new token to keycloak using refresh token + tags: + - Keycloak /reference/diagnosa/: get: parameters: diff --git a/internal/config/config.go b/internal/config/config.go index 73715cd..2179fe7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,6 +28,7 @@ func LoadConfig() *Config { Realm: getEnv("KEYCLOAK_REALM", "sandbox"), Audience: getEnv("KEYCLOAK_AUDIENCE", "akbar-test"), Issuer: getEnv("KEYCLOAK_ISSUER", "https://auth.rssa.top/realms/sandbox"), + SecretKey: getEnv("KEYCLOAK_SECRET_KEY", ""), IsEnabled: getEnvAsBool("KEYCLOAK_IS_ENABLE", false), }, } diff --git a/internal/config/struct.go b/internal/config/struct.go index d964b99..5264c57 100644 --- a/internal/config/struct.go +++ b/internal/config/struct.go @@ -55,5 +55,6 @@ type KeycloakConfig struct { Realm string Audience string Issuer string + SecretKey string IsEnabled bool } diff --git a/internal/domain/keycloak/handler.go b/internal/domain/keycloak/handler.go new file mode 100644 index 0000000..bc431f8 --- /dev/null +++ b/internal/domain/keycloak/handler.go @@ -0,0 +1,47 @@ +package keycloak + +import ( + "net/http" + + baseResponse "antrian-operasi/internal/shared" + + "github.com/gin-gonic/gin" +) + +type KeycloakHandler struct { + service IKeycloakService +} + +func NewKeycloakHandler(service IKeycloakService) KeycloakHandler { + return KeycloakHandler{service} +} + +// GetTokenByRefreshCode godoc +// @Summary Requesting new token to keycloak using refresh token +// @Tags Keycloak +// @Param refresh_token query string true "Valid Refresh Token" +// @Success 200 {object} shared.BaseResponse +// @Failure 500 {object} shared.BaseErrorResponse +// @Router /keycloak/refresh-token [post] +func (h KeycloakHandler) GetTokenByRefreshCode(c *gin.Context) { + refreshToken := c.Query("refresh_token") + resp, err := h.service.FetchTokenUsingRefreshToken(c, refreshToken) + + if err != nil { + errorResponse := baseResponse.BaseErrorResponse{ + Success: false, + Code: 400, + Message: err.Error(), + } + c.JSON(http.StatusInternalServerError, errorResponse) + return + } + + response := baseResponse.ToBaseResponse( + resp, + true, + 200, + "success refreshing token") + + c.JSON(http.StatusOK, response) +} diff --git a/internal/domain/keycloak/responseKeycloak.go b/internal/domain/keycloak/responseKeycloak.go new file mode 100644 index 0000000..a633fcb --- /dev/null +++ b/internal/domain/keycloak/responseKeycloak.go @@ -0,0 +1,15 @@ +package keycloak + +type TokenErrorResponse struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + RefreshExpiresIn int `json:"refresh_expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` +} diff --git a/internal/domain/keycloak/routes.go b/internal/domain/keycloak/routes.go new file mode 100644 index 0000000..8b116ee --- /dev/null +++ b/internal/domain/keycloak/routes.go @@ -0,0 +1,14 @@ +package keycloak + +import ( + "antrian-operasi/internal/config" + + "github.com/gin-gonic/gin" +) + +func RegisterRoutes(r *gin.RouterGroup, cfg config.KeycloakConfig) { + keycloakService := NewKeycloakService(cfg) + keycloakHandler := NewKeycloakHandler(keycloakService) + + r.POST("/refresh-token", keycloakHandler.GetTokenByRefreshCode) +} diff --git a/internal/domain/keycloak/service.go b/internal/domain/keycloak/service.go new file mode 100644 index 0000000..8ed3e1e --- /dev/null +++ b/internal/domain/keycloak/service.go @@ -0,0 +1,61 @@ +package keycloak + +import ( + "antrian-operasi/internal/config" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "strings" +) + +type IKeycloakService interface { + FetchTokenUsingRefreshToken(c context.Context, refreshToken string) (*TokenResponse, error) +} + +type KeycloakService struct { + config config.KeycloakConfig +} + +func NewKeycloakService(cfg config.KeycloakConfig) IKeycloakService { + return KeycloakService{cfg} +} + +func (s KeycloakService) FetchTokenUsingRefreshToken(c context.Context, refreshToken string) (*TokenResponse, error) { + refreshTokenUrl := s.config.BaseUrl + "/realms/" + s.config.Realm + "/protocol/openid-connect/token" + bodyRequest := url.Values{} + bodyRequest.Set("grant_type", "refresh_token") + bodyRequest.Set("client_id", s.config.Audience) + bodyRequest.Set("client_secret", s.config.SecretKey) + bodyRequest.Set("refresh_token", refreshToken) + + client := &http.Client{} + req, err := http.NewRequestWithContext(c, http.MethodPost, refreshTokenUrl, strings.NewReader(bodyRequest.Encode())) + if err != nil { + log.Printf("error request token %s", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + log.Printf("error response %s", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp TokenErrorResponse + json.NewDecoder(resp.Body).Decode(&errResp) + + return nil, fmt.Errorf("Keycloak error : %s - %s", errResp.Error, errResp.ErrorDescription) + } + + var tokenResp TokenResponse + err = json.NewDecoder(resp.Body).Decode(&tokenResp) + if err != nil { + return nil, err + } + + return &tokenResp, nil +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 9fe3a88..921b6d4 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -5,6 +5,7 @@ import ( "antrian-operasi/internal/database" antrianoperasi "antrian-operasi/internal/domain/antrian_operasi" "antrian-operasi/internal/domain/dashboard" + "antrian-operasi/internal/domain/keycloak" "antrian-operasi/internal/domain/reference/diagnosa" "antrian-operasi/internal/domain/reference/dokter" "antrian-operasi/internal/domain/reference/kategori" @@ -40,13 +41,13 @@ func RegisterRoutes(cfg *config.Config, dbService database.Service) *gin.Engine log.Fatalf("Unable to initiate keycloak auth") } - api := router.Group("/api", authKeycloak) + api := router.Group("/api") - antrian := api.Group("/antrian-operasi") + antrian := api.Group("/antrian-operasi", authKeycloak) { antrianoperasi.RegisterRoutes(antrian, dbService) } - reference := api.Group("/reference") + reference := api.Group("/reference", authKeycloak) { kategori.RegisterRoutes(reference, dbService) spesialis.RegisterRoutes(reference, dbService) @@ -55,10 +56,14 @@ func RegisterRoutes(cfg *config.Config, dbService database.Service) *gin.Engine diagnosa.RegisterRoutes(reference, dbService) tindakan.RegisterRoutes(reference, dbService) } - dboard := api.Group("dashboard") + dboard := api.Group("dashboard", authKeycloak) { dashboard.RegisterRoutes(dboard, dbService) } + kc := api.Group("/keycloak") + { + keycloak.RegisterRoutes(kc, cfg.Keycloak) + } return router }