first commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package auth
|
||||
|
||||
// RefreshTokenRequest mendefinisikan struktur untuk request refresh token.
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
// Provider membantu membedakan antara "jwt" dan "keycloak".
|
||||
// Ini membuat endpoint menjadi fleksibel.
|
||||
Provider string `json:"provider" binding:"required,oneof=jwt keycloak"`
|
||||
}
|
||||
|
||||
// LoginResponse adalah respons standar untuk login dan refresh token.
|
||||
type LoginResponse struct {
|
||||
Provider string `json:"provider,omitempty"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
IdToken string `json:"id_token,omitempty"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
RoleID int64 `json:"role_id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
Id int64 `json:"id" db:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Password string `json:"-" db:"password"` // Hidden dari response JSON
|
||||
RoleID *int64 `json:"role_id" db:"role_id"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
CreatedAt *time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CommandRepository interface {
|
||||
CreateUser(ctx context.Context, user *User) error
|
||||
UpdateUser(ctx context.Context, user *User) error
|
||||
ChangePasswordInTx(ctx context.Context, userID int64, newPassword string) error
|
||||
RevokeAndSaveTokens(ctx context.Context, oldToken string, newRefreshToken string) error
|
||||
}
|
||||
|
||||
type QueryRepository interface {
|
||||
FindUserByEmail(ctx context.Context, email string) (*User, error)
|
||||
FindUserByID(ctx context.Context, id int64) (*User, error)
|
||||
FindRefreshToken(ctx context.Context, token string) (*StoredToken, error)
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
dbName string
|
||||
qb query.QueryBuilder
|
||||
dbType query.DBType
|
||||
}
|
||||
|
||||
func newRepository(dbManager database.Service, dbName string) *repository {
|
||||
dbType := query.DBTypePostgreSQL
|
||||
allowedColumns := []string{
|
||||
"Id",
|
||||
"Email",
|
||||
"Password",
|
||||
"RoleID",
|
||||
"Active",
|
||||
"CreatedAt",
|
||||
"UpdatedAt",
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(dbType).
|
||||
SetSecurityOptions(true, 1000).
|
||||
SetQueryLogging(true).
|
||||
SetQueryTimeout(30).
|
||||
SetAllowedColumns(allowedColumns)
|
||||
return &repository{dbManager: dbManager, dbName: dbName, qb: qb, dbType: dbType}
|
||||
}
|
||||
|
||||
func NewCommandRepository(dbManager database.Service, dbName string) CommandRepository {
|
||||
return newRepository(dbManager, dbName)
|
||||
}
|
||||
|
||||
func NewQueryRepository(dbManager database.Service, dbName string) QueryRepository {
|
||||
return newRepository(dbManager, dbName)
|
||||
}
|
||||
|
||||
func (r *repository) getWriteGormDB() (*gorm.DB, error) {
|
||||
return r.dbManager.GetGormDB(r.dbName)
|
||||
}
|
||||
|
||||
func (r *repository) getReadSQLXDB() (*sqlx.DB, error) {
|
||||
sqlDB, err := r.dbManager.GetReadDB(r.dbName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get read db: %w", err)
|
||||
}
|
||||
return sqlx.NewDb(sqlDB, "pgx"), nil
|
||||
}
|
||||
|
||||
func (r *repository) getWriteSQLXDB() (*sqlx.DB, error) {
|
||||
// Mengambil koneksi database utama (tulis)
|
||||
sqlDB, err := r.dbManager.GetDB(r.dbName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get write db: %w", err)
|
||||
}
|
||||
return sqlx.NewDb(sqlDB, "pgx"), nil
|
||||
}
|
||||
|
||||
func (r *repository) CreateUser(ctx context.Context, user *User) error {
|
||||
db, err := r.getWriteGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
func (r *repository) UpdateUser(ctx context.Context, user *User) error {
|
||||
db, err := r.getWriteGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
func (r *repository) FindUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
sqlxDB, err := r.getReadSQLXDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user User
|
||||
q := query.DynamicQuery{
|
||||
From: "users", // GORM default table name for User struct
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateEqualFilter("Email", email),
|
||||
},
|
||||
}},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &user); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil // Not found is not an error, service layer will handle it
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find user by email: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *repository) ChangePasswordInTx(ctx context.Context, userID int64, newPassword string) (err error) {
|
||||
// 1. Dapatkan koneksi database tulis
|
||||
writeDB, err := r.getWriteSQLXDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. Mulai transaksi
|
||||
tx, err := writeDB.Beginx()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
// 3. Defer Rollback. Ini akan dieksekusi jika fungsi return error atau panic.
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
tx.Rollback()
|
||||
panic(p) // Lanjutkan panic setelah rollback
|
||||
} else if err != nil {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
err = fmt.Errorf("transaction error: %v, rollback error: %v", err, rbErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 4. Operasi 1: Update password pengguna
|
||||
updateData := query.UpdateData{
|
||||
Columns: []string{"Password", "UpdatedAt"},
|
||||
Values: []interface{}{newPassword, time.Now()},
|
||||
}
|
||||
filters := []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{query.CreateEqualFilter("Id", userID)},
|
||||
}}
|
||||
|
||||
// Berikan objek transaksi 'tx' ke Query Builder
|
||||
if _, err = r.qb.ExecuteUpdate(ctx, tx, "users", updateData, filters); err != nil {
|
||||
return fmt.Errorf("failed to update password in tx: %w", err)
|
||||
}
|
||||
|
||||
// 5. Operasi 2: Catat aktivitas
|
||||
logData := query.InsertData{
|
||||
Columns: []string{"user_id", "activity", "created_at"},
|
||||
Values: []interface{}{userID, "password changed via transaction", time.Now()},
|
||||
}
|
||||
|
||||
// Gunakan objek transaksi 'tx' yang sama
|
||||
if _, err = r.qb.ExecuteInsert(ctx, tx, "user_activity_logs", logData); err != nil {
|
||||
// Asumsikan tabel 'user_activity_logs' ada
|
||||
return fmt.Errorf("failed to log activity in tx: %w", err)
|
||||
}
|
||||
|
||||
// 6. Jika semua berhasil, commit transaksi
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *repository) FindUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
sqlxDB, err := r.getReadSQLXDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user User
|
||||
q := query.DynamicQuery{
|
||||
From: "users", // GORM default table name for User struct
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateEqualFilter("Id", id),
|
||||
},
|
||||
}},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &user); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil // Not found is not an error, service layer will handle it
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find user by id: %w", err)
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *repository) FindRefreshToken(ctx context.Context, token string) (*StoredToken, error) {
|
||||
sqlxDB, err := r.getReadSQLXDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stored StoredToken
|
||||
q := query.DynamicQuery{
|
||||
From: "refresh_tokens",
|
||||
Filters: []query.FilterGroup{{
|
||||
Filters: []query.DynamicFilter{query.CreateEqualFilter("Token", token)},
|
||||
}},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
if err := r.qb.ExecuteQueryRow(ctx, sqlxDB, q, &stored); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find refresh token: %w", err)
|
||||
}
|
||||
return &stored, nil
|
||||
}
|
||||
|
||||
func (r *repository) RevokeAndSaveTokens(ctx context.Context, oldToken string, newRefreshToken string) error {
|
||||
// Implementasi sederhana rotasi token
|
||||
writeDB, err := r.getWriteGormDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Sesuaikan dengan struktur tabel Refresh Token yang Anda miliki di database
|
||||
// Ini adalah stub untuk memenuhi kontrak interface
|
||||
return writeDB.WithContext(ctx).Exec("UPDATE refresh_tokens SET is_revoked = ? WHERE token = ?", true, oldToken).Error
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"service/internal/infrastructure/cache"
|
||||
"service/internal/infrastructure/config"
|
||||
"service/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type StoredToken struct {
|
||||
Token string
|
||||
IsRevoked bool
|
||||
UserID int64
|
||||
ExpiresAt int64
|
||||
}
|
||||
|
||||
func (t *StoredToken) IsExpired() bool { return false /* implementasi */ }
|
||||
|
||||
// Service mendefinisikan interface untuk operasi otentikasi.
|
||||
type Service interface {
|
||||
RefreshToken(ctx context.Context, req RefreshTokenRequest) (*LoginResponse, error)
|
||||
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
|
||||
Register(ctx context.Context, req RegisterRequest) (interface{}, error)
|
||||
Logout(ctx context.Context, token string) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
cmdRepo CommandRepository
|
||||
queryRepo QueryRepository
|
||||
cache *cache.Manager
|
||||
config *config.Config // Ganti keycloakClient dengan config
|
||||
}
|
||||
|
||||
// NewService membuat instance service auth baru.
|
||||
// Signature diperbarui untuk menerima config secara keseluruhan.
|
||||
func NewService(cmdRepo CommandRepository, queryRepo QueryRepository, cache *cache.Manager, cfg *config.Config) Service {
|
||||
return &service{
|
||||
cmdRepo: cmdRepo,
|
||||
queryRepo: queryRepo,
|
||||
cache: cache,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Login mengimplementasikan otentikasi menggunakan data dummy untuk keperluan testing.
|
||||
func (s *service) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
||||
// Simulasi pengecekan kredensial dengan beberapa akun dummy
|
||||
type dummyUser struct {
|
||||
Password string
|
||||
UserID string
|
||||
RoleID string
|
||||
Name string
|
||||
}
|
||||
|
||||
dummyUsers := map[string]dummyUser{
|
||||
"[email protected]": {"password123", "1001", "admin", "Admin Dummy"},
|
||||
"[email protected]": {"password123", "1002", "user", "User Dummy"},
|
||||
"[email protected]": {"password123", "1003", "manager", "Manager Dummy"},
|
||||
}
|
||||
|
||||
user, exists := dummyUsers[req.Email]
|
||||
if !exists || user.Password != req.Password {
|
||||
return nil, errors.UnauthorizedError().Message("Email atau password salah. Coba: [email protected], [email protected], [email protected] (password123)").Build()
|
||||
}
|
||||
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
secret = "fallback_secret_key_change_in_production"
|
||||
}
|
||||
|
||||
// Buat Access Token baru (Umur pendek, misal 15 Menit)
|
||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.UserID,
|
||||
"email": req.Email,
|
||||
"role_id": user.RoleID,
|
||||
"name": user.Name,
|
||||
"exp": time.Now().Add(60 * time.Minute).Unix(),
|
||||
})
|
||||
newAccessTokenStr, _ := accessToken.SignedString([]byte(secret))
|
||||
|
||||
// Buat Refresh Token baru (Umur panjang, misal 7 Hari)
|
||||
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.UserID,
|
||||
"email": req.Email,
|
||||
"role_id": user.RoleID,
|
||||
"name": user.Name,
|
||||
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||
})
|
||||
newRefreshTokenStr, _ := refreshToken.SignedString([]byte(secret))
|
||||
|
||||
return &LoginResponse{
|
||||
Provider: "jwt",
|
||||
AccessToken: newAccessTokenStr,
|
||||
RefreshToken: newRefreshTokenStr,
|
||||
ExpiresIn: 900, // 15 menit dalam detik
|
||||
TokenType: "Bearer",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register mengimplementasikan pendaftaran user dengan respons data json dummy.
|
||||
func (s *service) Register(ctx context.Context, req RegisterRequest) (interface{}, error) {
|
||||
// Mengembalikan response dummy seolah-olah user berhasil dibuat di DB
|
||||
return map[string]interface{}{
|
||||
"id": 1002, // Generated Dummy ID
|
||||
"name": req.Name,
|
||||
"email": req.Email,
|
||||
"role_id": req.RoleID,
|
||||
"created_at": time.Now().Format(time.RFC3339),
|
||||
"status": "active",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Logout mengimplementasikan proses logout dengan memasukkan token ke cache blacklist.
|
||||
func (s *service) Logout(ctx context.Context, token string) error {
|
||||
if token == "" {
|
||||
return errors.NewValidationError().Message("Token tidak valid").Build()
|
||||
}
|
||||
|
||||
// Invalidate token di cache (Blacklist) jika cache manager aktif
|
||||
if s.cache != nil {
|
||||
// Set token ke dalam blacklist dengan durasi 15 menit (Sesuai umur access token)
|
||||
_ = s.cache.Set(ctx, "blacklist_token:"+token, true, 15*time.Minute)
|
||||
}
|
||||
|
||||
// TODO: Segera hapus/tandai Refresh Token sebagai revoked di database agar tidak bisa
|
||||
// digunakan lagi untuk generate access token baru setelah pengguna berhasil logout.
|
||||
// _ = s.cmdRepo.RevokeAllTokensByToken(ctx, token)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshToken menangani refresh token untuk berbagai provider.
|
||||
func (s *service) RefreshToken(ctx context.Context, req RefreshTokenRequest) (*LoginResponse, error) {
|
||||
switch req.Provider {
|
||||
case "jwt":
|
||||
return s.refreshInternalJWT(ctx, req.RefreshToken)
|
||||
case "keycloak":
|
||||
return s.refreshKeycloakToken(ctx, req.RefreshToken)
|
||||
|
||||
default:
|
||||
return nil, errors.NewValidationError().Message("Provider tidak didukung").Metadata("provider", req.Provider).Build()
|
||||
}
|
||||
}
|
||||
|
||||
// refreshInternalJWT menangani validasi dan pembuatan JWT internal baru.
|
||||
func (s *service) refreshInternalJWT(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
secret = "fallback_secret_key_change_in_production"
|
||||
}
|
||||
|
||||
// Parse dan validasi refresh token
|
||||
parsedToken, err := jwt.Parse(refreshToken, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method")
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !parsedToken.Valid {
|
||||
return nil, errors.UnauthorizedError().Message("Refresh token tidak valid atau telah kedaluwarsa").Build()
|
||||
}
|
||||
|
||||
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.UnauthorizedError().Message("Klaim token tidak valid").Build()
|
||||
}
|
||||
|
||||
// Cek apakah ini dummy user dari fungsi Login. Jika ya, lewati pengecekan DB.
|
||||
isDummyUser := false
|
||||
if userID, ok := claims["user_id"].(string); ok && (userID == "1001" || userID == "1002" || userID == "1003") {
|
||||
isDummyUser = true
|
||||
}
|
||||
|
||||
// Hanya lakukan pengecekan ke database jika bukan dummy user
|
||||
if !isDummyUser {
|
||||
existingToken, err := s.queryRepo.FindRefreshToken(ctx, refreshToken)
|
||||
if err != nil {
|
||||
// Kesalahan saat query ke database
|
||||
return nil, errors.InternalError().Message("Gagal memvalidasi refresh token").Cause(err).Build()
|
||||
}
|
||||
if existingToken == nil {
|
||||
return nil, errors.UnauthorizedError().Message("Refresh token tidak valid atau tidak ditemukan").Build()
|
||||
}
|
||||
if existingToken.IsRevoked {
|
||||
return nil, errors.UnauthorizedError().Message("Refresh token telah dicabut (dibatalkan)").Metadata("reason", "revoked").Build()
|
||||
}
|
||||
}
|
||||
|
||||
// Buat Access Token baru (Umur pendek, misal 15 Menit)
|
||||
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": claims["user_id"],
|
||||
"email": claims["email"],
|
||||
"role_id": claims["role_id"],
|
||||
"name": claims["name"],
|
||||
"exp": time.Now().Add(15 * time.Minute).Unix(),
|
||||
})
|
||||
newAccessTokenStr, _ := accessToken.SignedString([]byte(secret))
|
||||
|
||||
// Buat Refresh Token baru (Umur panjang, misal 7 Hari)
|
||||
newRefreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": claims["user_id"],
|
||||
"email": claims["email"],
|
||||
"role_id": claims["role_id"],
|
||||
"name": claims["name"],
|
||||
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||
})
|
||||
newRefreshTokenStr, _ := newRefreshToken.SignedString([]byte(secret))
|
||||
|
||||
// Revoke token lama dan simpan yang baru (hanya untuk user non-dummy)
|
||||
if !isDummyUser {
|
||||
_ = s.cmdRepo.RevokeAndSaveTokens(ctx, refreshToken, newRefreshTokenStr)
|
||||
}
|
||||
|
||||
return &LoginResponse{
|
||||
Provider: "jwt",
|
||||
AccessToken: newAccessTokenStr,
|
||||
RefreshToken: newRefreshTokenStr,
|
||||
ExpiresIn: 900, // 15 menit dalam detik
|
||||
TokenType: "Bearer",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// refreshKeycloakToken menangani refresh token melalui Keycloak.
|
||||
func (s *service) refreshKeycloakToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
||||
if !s.config.Keycloak.Enabled || s.config.Keycloak.URL == "" {
|
||||
return nil, errors.InternalError().Message("Provider Keycloak tidak dikonfigurasi dengan benar").Build()
|
||||
}
|
||||
|
||||
tokenURL := fmt.Sprintf("%s/realms/%s/protocol/openid-connect/token", s.config.Keycloak.URL, s.config.Keycloak.Realm)
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "refresh_token")
|
||||
data.Set("client_id", s.config.Keycloak.ClientID)
|
||||
data.Set("client_secret", s.config.Keycloak.ClientSecret)
|
||||
data.Set("refresh_token", refreshToken)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewBufferString(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, errors.InternalError().Cause(err).Message("Gagal membuat request ke Keycloak").Build()
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.InternalError().Cause(err).Message("Gagal berkomunikasi dengan Keycloak").Build()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var keycloakErr map[string]interface{}
|
||||
_ = json.NewDecoder(resp.Body).Decode(&keycloakErr)
|
||||
return nil, errors.UnauthorizedError().
|
||||
Message("Gagal me-refresh token dengan Keycloak").
|
||||
Metadata("upstream_status", resp.StatusCode).
|
||||
Metadata("upstream_error", keycloakErr).Build()
|
||||
}
|
||||
|
||||
var tokenResponse LoginResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
|
||||
return nil, errors.InternalError().Cause(err).Message("Gagal mem-parsing respons Keycloak").Build()
|
||||
}
|
||||
|
||||
// Tambahkan provider ke response
|
||||
tokenResponse.Provider = "keycloak"
|
||||
|
||||
return &tokenResponse, nil
|
||||
}
|
||||
Reference in New Issue
Block a user