first commit
This commit is contained in:
28
internal/pkg/utils/file.go
Normal file
28
internal/pkg/utils/file.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DeleteLocalFile menghapus file lokal jika file tersebut berada di folder ./public
|
||||
func DeleteLocalFile(relPath string) {
|
||||
if relPath == "" || strings.HasPrefix(relPath, "http://") || strings.HasPrefix(relPath, "https://") {
|
||||
return
|
||||
}
|
||||
|
||||
cleaned := filepath.Clean(relPath)
|
||||
var localPath string
|
||||
if strings.HasPrefix(cleaned, "/images/") {
|
||||
localPath = "./public" + cleaned
|
||||
} else if strings.HasPrefix(cleaned, "/public/") {
|
||||
localPath = "." + cleaned
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(localPath); err == nil {
|
||||
_ = os.Remove(localPath)
|
||||
}
|
||||
}
|
||||
95
internal/pkg/utils/jwt.go
Normal file
95
internal/pkg/utils/jwt.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"cardverse/config"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateAccessToken(userID uint, email string, role string) (string, error) {
|
||||
cfg := config.Cfg
|
||||
expiration := time.Now().Add(time.Duration(cfg.JWTExpiresHours) * time.Hour)
|
||||
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiration),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(cfg.JWTSecret))
|
||||
}
|
||||
|
||||
func GenerateRefreshToken(userID uint) (string, error) {
|
||||
cfg := config.Cfg
|
||||
expiration := time.Now().Add(time.Duration(cfg.JWTRefreshExpiresHours) * time.Hour)
|
||||
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiration),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(cfg.JWTRefreshSecret))
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, email string, role string) (string, error) {
|
||||
return GenerateAccessToken(userID, email, role)
|
||||
}
|
||||
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
cfg := config.Cfg
|
||||
claims := &Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("metode signing token tidak valid")
|
||||
}
|
||||
return []byte(cfg.JWTSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, errors.New("token tidak valid")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func ValidateRefreshToken(tokenString string) (*Claims, error) {
|
||||
cfg := config.Cfg
|
||||
claims := &Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("metode signing token tidak valid")
|
||||
}
|
||||
return []byte(cfg.JWTRefreshSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, errors.New("refresh token tidak valid")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
67
internal/pkg/utils/jwt_test.go
Normal file
67
internal/pkg/utils/jwt_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cardverse/config"
|
||||
)
|
||||
|
||||
func setupTestConfig() {
|
||||
config.Cfg = &config.Config{
|
||||
JWTSecret: "test-secret-key",
|
||||
JWTExpiresHours: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateToken_AndValidateToken(t *testing.T) {
|
||||
setupTestConfig()
|
||||
|
||||
token, err := GenerateToken(42, "user@example.com", "admin")
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateToken expected no error, got: %v", err)
|
||||
}
|
||||
if token == "" {
|
||||
t.Fatal("token should not be empty")
|
||||
}
|
||||
|
||||
claims, err := ValidateToken(token)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken expected no error for valid token, got: %v", err)
|
||||
}
|
||||
|
||||
if claims.UserID != 42 {
|
||||
t.Errorf("UserID expected 42, got %d", claims.UserID)
|
||||
}
|
||||
if claims.Email != "user@example.com" {
|
||||
t.Errorf("Email expected user@example.com, got %s", claims.Email)
|
||||
}
|
||||
if claims.Role != "admin" {
|
||||
t.Errorf("Role expected admin, got %s", claims.Role)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateToken_InvalidToken(t *testing.T) {
|
||||
setupTestConfig()
|
||||
|
||||
_, err := ValidateToken("invalid.random.token")
|
||||
if err == nil {
|
||||
t.Fatal("ValidateToken should fail for invalid token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateToken_ExpiredToken(t *testing.T) {
|
||||
config.Cfg = &config.Config{
|
||||
JWTSecret: "test-secret-key",
|
||||
JWTExpiresHours: 0,
|
||||
}
|
||||
|
||||
token, _ := GenerateToken(1, "expired@example.com", "user")
|
||||
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
|
||||
_, err := ValidateToken(token)
|
||||
if err == nil {
|
||||
t.Fatal("ValidateToken should fail for expired token")
|
||||
}
|
||||
}
|
||||
15
internal/pkg/utils/password.go
Normal file
15
internal/pkg/utils/password.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// HashPassword meng-hash plain password menggunakan bcrypt
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPassword membandingkan plain password dengan hash yang tersimpan
|
||||
func CheckPassword(hashedPassword, plainPassword string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
|
||||
return err == nil
|
||||
}
|
||||
34
internal/pkg/utils/password_test.go
Normal file
34
internal/pkg/utils/password_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHashPassword_AndCheckPassword(t *testing.T) {
|
||||
plain := "rahasia123"
|
||||
|
||||
hashed, err := HashPassword(plain)
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword tidak boleh error, dapat: %v", err)
|
||||
}
|
||||
|
||||
if hashed == plain {
|
||||
t.Fatal("hasil hash tidak boleh sama dengan plain password")
|
||||
}
|
||||
|
||||
if !CheckPassword(hashed, plain) {
|
||||
t.Fatal("CheckPassword harus true untuk password yang benar")
|
||||
}
|
||||
|
||||
if CheckPassword(hashed, "password_salah") {
|
||||
t.Fatal("CheckPassword harus false untuk password yang salah")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashPassword_HasilBerbedaSetiapKaliDipanggil(t *testing.T) {
|
||||
// bcrypt menyisipkan salt acak, jadi 2x hash dari password sama harus menghasilkan string berbeda
|
||||
hash1, _ := HashPassword("sama-sama")
|
||||
hash2, _ := HashPassword("sama-sama")
|
||||
|
||||
if hash1 == hash2 {
|
||||
t.Fatal("dua hash dari password yang sama seharusnya berbeda karena salt")
|
||||
}
|
||||
}
|
||||
31
internal/pkg/utils/string.go
Normal file
31
internal/pkg/utils/string.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var nonAlphanumericRegexp = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
// UnderscoreString mengubah string judul/nama menjadi format_ular (misal: "Evolusi Mega" -> "evolusi_mega")
|
||||
func UnderscoreString(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
s = strings.ReplaceAll(s, "&", "and")
|
||||
s = nonAlphanumericRegexp.ReplaceAllString(s, "_")
|
||||
s = strings.Trim(s, "_")
|
||||
if s == "" {
|
||||
s = "underscore_string"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Slugify(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
s = strings.ReplaceAll(s, "&", "and")
|
||||
s = nonAlphanumericRegexp.ReplaceAllString(s, "-")
|
||||
s = strings.Trim(s, "-")
|
||||
if s == "" {
|
||||
s = "slugify"
|
||||
}
|
||||
return s
|
||||
}
|
||||
77
internal/pkg/utils/url.go
Normal file
77
internal/pkg/utils/url.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"cardverse/config"
|
||||
)
|
||||
|
||||
// getBaseURL mengambil Base URL dari config (APP_BASE_URL) atau environment variable dengan fallback ke http://localhost:PORT
|
||||
func getBaseURL() string {
|
||||
if config.Cfg != nil && config.Cfg.AppBaseURL != "" {
|
||||
return strings.TrimRight(config.Cfg.AppBaseURL, "/")
|
||||
}
|
||||
|
||||
if envURL := os.Getenv("APP_BASE_URL"); envURL != "" {
|
||||
return strings.TrimRight(envURL, "/")
|
||||
}
|
||||
|
||||
port := os.Getenv("APP_PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
return "http://localhost:" + port
|
||||
}
|
||||
|
||||
// FormatMediaURL mengubah path relatif gambar (seperti /images/...) menjadi URL lengkap menggunakan APP_BASE_URL
|
||||
func FormatMediaURL(urlStr string) string {
|
||||
if urlStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Jika sudah berupa URL lengkap (http:// atau https://), kembalikan langsung
|
||||
if strings.HasPrefix(urlStr, "http://") || strings.HasPrefix(urlStr, "https://") {
|
||||
return urlStr
|
||||
}
|
||||
|
||||
baseURL := getBaseURL()
|
||||
|
||||
if !strings.HasPrefix(urlStr, "/") {
|
||||
urlStr = "/" + urlStr
|
||||
}
|
||||
|
||||
return baseURL + urlStr
|
||||
}
|
||||
|
||||
// FormatMediaURLPtr mengubah pointer path relatif gambar menjadi pointer URL lengkap
|
||||
func FormatMediaURLPtr(urlPtr *string) *string {
|
||||
if urlPtr == nil || *urlPtr == "" {
|
||||
return urlPtr
|
||||
}
|
||||
formatted := FormatMediaURL(*urlPtr)
|
||||
return &formatted
|
||||
}
|
||||
|
||||
// FormatJSONBMediaURLs mengubah seluruh path relatif gambar (/images/... atau /public/...) di dalam data byte JSONB menjadi URL lengkap
|
||||
func FormatJSONBMediaURLs(data []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
baseURL := getBaseURL()
|
||||
s := string(data)
|
||||
|
||||
// Replace "/images/" dengan "APP_BASE_URL/images/"
|
||||
targetImages := `"/images/`
|
||||
replaceImages := `"` + baseURL + `/images/`
|
||||
s = strings.ReplaceAll(s, targetImages, replaceImages)
|
||||
|
||||
// Replace "/public/" dengan "APP_BASE_URL/public/"
|
||||
targetPublic := `"/public/`
|
||||
replacePublic := `"` + baseURL + `/public/`
|
||||
s = strings.ReplaceAll(s, targetPublic, replacePublic)
|
||||
|
||||
return []byte(s)
|
||||
}
|
||||
Reference in New Issue
Block a user