Initial commit - Lost and Found Revisi

This commit is contained in:
bambang-code1
2025-12-20 00:01:08 +07:00
commit f4c838471c
210 changed files with 30706 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
// internal/utils/encryption.go
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
"os"
)
// ✅ KRITERIA BASDAT: Keamanan Data - Enkripsi untuk Data Sensitif (5%)
var encryptionKey []byte
// InitEncryption inisialisasi encryption key dari environment
func InitEncryption() error {
key := os.Getenv("ENCRYPTION_KEY")
if key == "" {
// Generate random key untuk development (TIDAK untuk production!)
key = "32-byte-long-encryption-key!!" // 32 bytes untuk AES-256
}
if len(key) != 32 {
return errors.New("encryption key must be exactly 32 bytes for AES-256")
}
encryptionKey = []byte(key)
return nil
}
// EncryptString mengenkripsi string menggunakan AES-256-GCM
// Digunakan untuk data sensitif: password, NRP, phone, contact info
func EncryptString(plaintext string) (string, error) {
if encryptionKey == nil {
if err := InitEncryption(); err != nil {
return "", err
}
}
// Create AES cipher
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return "", err
}
// Create GCM mode (Galois/Counter Mode) - authenticated encryption
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// Generate nonce (number used once)
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// Encrypt and authenticate
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
// Encode to base64 untuk storage di database
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// DecryptString mendekripsi string yang sudah dienkripsi
func DecryptString(encrypted string) (string, error) {
if encryptionKey == nil {
if err := InitEncryption(); err != nil {
return "", err
}
}
// Decode from base64
ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return "", err
}
// Create AES cipher
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return "", err
}
// Create GCM mode
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// Extract nonce
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
// Decrypt and verify
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// EncryptSensitiveFields helper untuk encrypt multiple fields
func EncryptSensitiveFields(fields map[string]string) (map[string]string, error) {
encrypted := make(map[string]string)
for key, value := range fields {
if value == "" {
encrypted[key] = ""
continue
}
encValue, err := EncryptString(value)
if err != nil {
return nil, err
}
encrypted[key] = encValue
}
return encrypted, nil
}
// DecryptSensitiveFields helper untuk decrypt multiple fields
func DecryptSensitiveFields(fields map[string]string) (map[string]string, error) {
decrypted := make(map[string]string)
for key, value := range fields {
if value == "" {
decrypted[key] = ""
continue
}
decValue, err := DecryptString(value)
if err != nil {
return nil, err
}
decrypted[key] = decValue
}
return decrypted, nil
}
// MaskSensitiveData untuk logging (mask sebagian data)
// Contoh: "081234567890" -> "0812****7890"
func MaskSensitiveData(data string) string {
if len(data) <= 8 {
return "****"
}
prefix := data[:4]
suffix := data[len(data)-4:]
return prefix + "****" + suffix
}
// ValidateEncryption test apakah encryption bekerja
func ValidateEncryption() error {
testData := "test-sensitive-data-12345"
encrypted, err := EncryptString(testData)
if err != nil {
return err
}
decrypted, err := DecryptString(encrypted)
if err != nil {
return err
}
if decrypted != testData {
return errors.New("encryption validation failed: data mismatch")
}
return nil
}
+67
View File
@@ -0,0 +1,67 @@
// internal/utils/error.go
package utils
import "fmt"
// AppError represents a custom application error
type AppError struct {
Code string
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// NewAppError creates a new application error
func NewAppError(code, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
// Common error codes
const (
ErrCodeValidation = "VALIDATION_ERROR"
ErrCodeNotFound = "NOT_FOUND"
ErrCodeUnauthorized = "UNAUTHORIZED"
ErrCodeForbidden = "FORBIDDEN"
ErrCodeInternal = "INTERNAL_ERROR"
ErrCodeDuplicate = "DUPLICATE_ERROR"
ErrCodeBadRequest = "BAD_REQUEST"
)
// Error constructors
func ValidationError(message string) *AppError {
return NewAppError(ErrCodeValidation, message, nil)
}
func NotFoundError(message string) *AppError {
return NewAppError(ErrCodeNotFound, message, nil)
}
func UnauthorizedError(message string) *AppError {
return NewAppError(ErrCodeUnauthorized, message, nil)
}
func ForbiddenError(message string) *AppError {
return NewAppError(ErrCodeForbidden, message, nil)
}
func InternalError(message string, err error) *AppError {
return NewAppError(ErrCodeInternal, message, err)
}
func DuplicateError(message string) *AppError {
return NewAppError(ErrCodeDuplicate, message, nil)
}
func BadRequestError(message string) *AppError {
return NewAppError(ErrCodeBadRequest, message, nil)
}
+100
View File
@@ -0,0 +1,100 @@
// internal/utils/excel_export.go
package utils
import (
"bytes"
"fmt"
"github.com/xuri/excelize/v2"
)
// ExcelExporter handles Excel file generation
type ExcelExporter struct {
file *excelize.File
sheetName string
rowIndex int
}
// NewExcelExporter creates a new Excel exporter
func NewExcelExporter() *ExcelExporter {
f := excelize.NewFile()
return &ExcelExporter{
file: f,
sheetName: "Sheet1",
rowIndex: 1,
}
}
// SetSheetName sets the active sheet name
func (e *ExcelExporter) SetSheetName(name string) {
e.file.SetSheetName("Sheet1", name)
e.sheetName = name
}
// AddRow adds a row of data
func (e *ExcelExporter) AddRow(data []string) error {
for colIndex, value := range data {
cell := fmt.Sprintf("%s%d", getColumnName(colIndex), e.rowIndex)
if err := e.file.SetCellValue(e.sheetName, cell, value); err != nil {
return err
}
}
e.rowIndex++
return nil
}
// AddHeader adds a header row with bold style
func (e *ExcelExporter) AddHeader(headers []string) error {
style, err := e.file.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
},
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#D3D3D3"},
Pattern: 1,
},
})
if err != nil {
return err
}
for colIndex, header := range headers {
cell := fmt.Sprintf("%s%d", getColumnName(colIndex), e.rowIndex)
if err := e.file.SetCellValue(e.sheetName, cell, header); err != nil {
return err
}
if err := e.file.SetCellStyle(e.sheetName, cell, cell, style); err != nil {
return err
}
}
e.rowIndex++
return nil
}
// AutoSizeColumns auto-sizes columns
func (e *ExcelExporter) AutoSizeColumns(columnCount int) {
for i := 0; i < columnCount; i++ {
col := getColumnName(i)
e.file.SetColWidth(e.sheetName, col, col, 15)
}
}
// Output returns the Excel file as bytes
func (e *ExcelExporter) Output() (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
if err := e.file.Write(buf); err != nil {
return nil, err
}
return buf, nil
}
// getColumnName converts column index to Excel column name (A, B, C, ..., AA, AB, ...)
func getColumnName(index int) string {
name := ""
for index >= 0 {
name = string(rune('A'+(index%26))) + name
index = index/26 - 1
}
return name
}
+18
View File
@@ -0,0 +1,18 @@
// internal/utils/hash.go
package utils
import (
"golang.org/x/crypto/bcrypt"
)
// HashPassword hashes a password using bcrypt
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPasswordHash compares a password with a hash
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
+210
View File
@@ -0,0 +1,210 @@
// internal/utils/image_handler.go
package utils
import (
"errors"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"regexp"
"github.com/nfnt/resize"
)
// ImageHandler handles image upload and processing
type ImageHandler struct {
uploadPath string
maxSize int64
allowedTypes []string
maxWidth uint
maxHeight uint
}
// NewImageHandler creates a new image handler
func NewImageHandler(uploadPath string) *ImageHandler {
return &ImageHandler{
uploadPath: uploadPath,
maxSize: 10 * 1024 * 1024, // 10MB
allowedTypes: []string{"image/jpeg", "image/jpg", "image/png"},
maxWidth: 1920,
maxHeight: 1080,
}
}
// UploadImage uploads and processes an image
// UploadImage uploads and processes an image
func (h *ImageHandler) UploadImage(file *multipart.FileHeader, subfolder string) (string, error) {
// Check file size
if file.Size > h.maxSize {
return "", errors.New("file size exceeds maximum allowed size")
}
// Check file type
if !h.isAllowedType(file.Header.Get("Content-Type")) {
return "", errors.New("file type not allowed")
}
// Open uploaded file
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
// ✅ GENERATE FILENAME DENGAN TIMESTAMP
ext := filepath.Ext(file.Filename)
timestamp := time.Now().Format("20060102_150405") // Format: YYYYMMDD_HHMMSS
baseFilename := strings.TrimSuffix(file.Filename, ext)
// Remove special characters from filename
baseFilename = sanitizeFilename(baseFilename)
filename := fmt.Sprintf("%s_%s%s", baseFilename, timestamp, ext)
// Create upload directory if not exists
uploadDir := filepath.Join(h.uploadPath, subfolder)
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
return "", err
}
// Full file path
filePath := filepath.Join(uploadDir, filename)
// Decode image
img, format, err := image.Decode(src)
if err != nil {
return "", errors.New("invalid image file")
}
// Resize if necessary
if uint(img.Bounds().Dx()) > h.maxWidth || uint(img.Bounds().Dy()) > h.maxHeight {
img = resize.Thumbnail(h.maxWidth, h.maxHeight, img, resize.Lanczos3)
}
// Create destination file
dst, err := os.Create(filePath)
if err != nil {
return "", err
}
defer dst.Close()
// Encode and save image
switch format {
case "jpeg", "jpg":
if err := jpeg.Encode(dst, img, &jpeg.Options{Quality: 90}); err != nil {
return "", err
}
case "png":
if err := png.Encode(dst, img); err != nil {
return "", err
}
default:
return "", errors.New("unsupported image format")
}
// Return relative path
return filepath.Join(subfolder, filename), nil
}
// ✅ TAMBAHKAN HELPER FUNCTION INI
func sanitizeFilename(filename string) string {
// Remove special characters, keep only alphanumeric and hyphens
reg := regexp.MustCompile(`[^a-zA-Z0-9\-]`)
sanitized := reg.ReplaceAllString(filename, "")
// Limit length
if len(sanitized) > 50 {
sanitized = sanitized[:50]
}
return sanitized
}
// UploadImageSimple uploads image without processing
func (h *ImageHandler) UploadImageSimple(file *multipart.FileHeader, subfolder string) (string, error) {
// Check file size
if file.Size > h.maxSize {
return "", errors.New("file size exceeds maximum allowed size")
}
// Check file type
if !h.isAllowedType(file.Header.Get("Content-Type")) {
return "", errors.New("file type not allowed")
}
// Open uploaded file
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
// Generate unique filename
ext := filepath.Ext(file.Filename)
filename := fmt.Sprintf("%d_%s%s", time.Now().Unix(), generateRandomString(8), ext)
// Create upload directory if not exists
uploadDir := filepath.Join(h.uploadPath, subfolder)
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
return "", err
}
// Full file path
filePath := filepath.Join(uploadDir, filename)
// Create destination file
dst, err := os.Create(filePath)
if err != nil {
return "", err
}
defer dst.Close()
// Copy file
if _, err := io.Copy(dst, src); err != nil {
return "", err
}
// Return relative path
return filepath.Join(subfolder, filename), nil
}
// DeleteImage deletes an image file
func (h *ImageHandler) DeleteImage(relativePath string) error {
if relativePath == "" {
return nil
}
filePath := filepath.Join(h.uploadPath, relativePath)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil // File doesn't exist, no error
}
return os.Remove(filePath)
}
// isAllowedType checks if file type is allowed
func (h *ImageHandler) isAllowedType(contentType string) bool {
for _, allowed := range h.allowedTypes {
if strings.EqualFold(contentType, allowed) {
return true
}
}
return false
}
// generateRandomString generates a random string
func generateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(b)
}
+76
View File
@@ -0,0 +1,76 @@
// internal/utils/matching.go
package utils
// CalculateMatchScore calculates match score between two items
func CalculateMatchScore(item1, item2 map[string]interface{}) float64 {
nameScore := 0.0
descScore := 0.0
// 1. Name Matching (Bobot 50%)
if name1, ok1 := item1["name"].(string); ok1 {
if name2, ok2 := item2["name"].(string); ok2 {
nameScore = CalculateStringSimilarity(name1, name2) * 100
}
}
// 2. Description/Secret Matching (Bobot 50%)
// Cek apakah ada secret_details, jika tidak pakai description
text1 := ""
if val, ok := item1["secret_details"].(string); ok && val != "" {
text1 = val
} else if val, ok := item1["description"].(string); ok {
text1 = val
}
text2 := ""
if val, ok := item2["secret_details"].(string); ok && val != "" {
text2 = val
} else if val, ok := item2["description"].(string); ok {
text2 = val
}
if text1 != "" && text2 != "" {
descScore = CalculateStringSimilarity(text1, text2) * 100
}
// Total Score = Rata-rata dari keduanya
totalScore := (nameScore * 0.5) + (descScore * 0.5)
return totalScore
}
// MatchItems matches items based on criteria
func MatchItems(lostItem, foundItems []map[string]interface{}, threshold float64) []map[string]interface{} {
var matches []map[string]interface{}
if len(lostItem) == 0 || len(foundItems) == 0 {
return matches
}
lost := lostItem[0]
for _, found := range foundItems {
score := CalculateMatchScore(lost, found)
if score >= threshold {
match := make(map[string]interface{})
match["item"] = found
match["score"] = score
match["level"] = getMatchLevel(score)
matches = append(matches, match)
}
}
return matches
}
// getMatchLevel returns match level based on score
func getMatchLevel(score float64) string {
if score >= 70 {
return "high"
} else if score >= 50 {
return "medium"
}
return "low"
}
+108
View File
@@ -0,0 +1,108 @@
// internal/utils/pdf_export.go
package utils
import (
"bytes"
"fmt"
"github.com/jung-kurt/gofpdf"
)
// PDFExporter handles PDF generation
type PDFExporter struct {
pdf *gofpdf.Fpdf
}
// NewPDFExporter creates a new PDF exporter
func NewPDFExporter() *PDFExporter {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.AddPage()
// ✅ Coba gunakan font built-in yang pasti ada
pdf.SetFont("Helvetica", "", 12) // Ganti dari "Arial" ke "Helvetica"
return &PDFExporter{
pdf: pdf,
}
}
// AddTitle adds a title to the PDF
func (e *PDFExporter) AddTitle(title string) {
e.pdf.SetFont("Arial", "B", 16)
e.pdf.Cell(0, 10, title)
e.pdf.Ln(12)
e.pdf.SetFont("Arial", "", 12)
}
// AddSubtitle adds a subtitle to the PDF
func (e *PDFExporter) AddSubtitle(subtitle string) {
e.pdf.SetFont("Arial", "I", 11)
e.pdf.Cell(0, 8, subtitle)
e.pdf.Ln(10)
e.pdf.SetFont("Arial", "", 12)
}
// AddText adds regular text
func (e *PDFExporter) AddText(text string) {
e.pdf.SetFont("Arial", "", 10)
e.pdf.MultiCell(0, 6, text, "", "", false)
e.pdf.Ln(4)
}
// AddNewLine adds a new line
func (e *PDFExporter) AddNewLine() {
e.pdf.Ln(6)
}
// AddTable adds a table to the PDF
func (e *PDFExporter) AddTable(headers []string, data [][]string) {
// Calculate column widths
pageWidth, _ := e.pdf.GetPageSize()
margins := 20.0 // Left + Right margins
tableWidth := pageWidth - margins
colWidth := tableWidth / float64(len(headers))
// Add headers
e.pdf.SetFont("Arial", "B", 10)
e.pdf.SetFillColor(200, 200, 200)
for _, header := range headers {
e.pdf.CellFormat(colWidth, 8, header, "1", 0, "C", true, 0, "")
}
e.pdf.Ln(-1)
// Add data rows
e.pdf.SetFont("Arial", "", 9)
e.pdf.SetFillColor(255, 255, 255)
fill := false
for _, row := range data {
for _, cell := range row {
if fill {
e.pdf.SetFillColor(245, 245, 245)
} else {
e.pdf.SetFillColor(255, 255, 255)
}
e.pdf.CellFormat(colWidth, 7, cell, "1", 0, "L", true, 0, "")
}
e.pdf.Ln(-1)
fill = !fill
}
}
// AddPageNumber adds page numbers
func (e *PDFExporter) AddPageNumber() {
e.pdf.AliasNbPages("")
e.pdf.SetY(-15)
e.pdf.SetFont("Arial", "I", 8)
e.pdf.CellFormat(0, 10, fmt.Sprintf("Halaman %d/{nb}", e.pdf.PageNo()), "", 0, "C", false, 0, "")
}
// Output returns the PDF as bytes
func (e *PDFExporter) Output() *bytes.Buffer {
var buf bytes.Buffer
err := e.pdf.Output(&buf)
if err != nil {
return nil
}
return &buf
}
+68
View File
@@ -0,0 +1,68 @@
// internal/utils/response.go
package utils
import (
"github.com/gin-gonic/gin"
)
// Response represents a standard API response
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// PaginatedResponse represents a paginated API response
type PaginatedResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data"`
Pagination Pagination `json:"pagination"`
}
// Pagination represents pagination metadata
type Pagination struct {
CurrentPage int `json:"current_page"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
TotalRecords int64 `json:"total_records"`
}
// SuccessResponse sends a success response
func SuccessResponse(ctx *gin.Context, statusCode int, message string, data interface{}) {
ctx.JSON(statusCode, Response{
Success: true,
Message: message,
Data: data,
})
}
// ErrorResponse sends an error response
func ErrorResponse(ctx *gin.Context, statusCode int, message string, error string) {
ctx.JSON(statusCode, Response{
Success: false,
Message: message,
Error: error,
})
}
// SendPaginatedResponse sends a paginated response (nama fungsi diubah untuk menghindari konflik)
func SendPaginatedResponse(ctx *gin.Context, statusCode int, message string, data interface{}, total int64, page, limit int) {
totalPages := int(total) / limit
if int(total)%limit != 0 {
totalPages++
}
ctx.JSON(statusCode, PaginatedResponse{
Success: true,
Message: message,
Data: data,
Pagination: Pagination{
CurrentPage: page,
PerPage: limit,
TotalPages: totalPages,
TotalRecords: total,
},
})
}
+160
View File
@@ -0,0 +1,160 @@
// internal/utils/similarity.go
package utils
import (
"math"
"strings"
"unicode"
)
// CalculateStringSimilarity calculates similarity between two strings using Levenshtein distance
func CalculateStringSimilarity(s1, s2 string) float64 {
// Normalize strings
s1 = normalizeString(s1)
s2 = normalizeString(s2)
if s1 == s2 {
return 1.0
}
if len(s1) == 0 || len(s2) == 0 {
return 0.0
}
// Calculate Levenshtein distance
distance := levenshteinDistance(s1, s2)
maxLen := math.Max(float64(len(s1)), float64(len(s2)))
similarity := 1.0 - (float64(distance) / maxLen)
return math.Max(0, similarity)
}
// levenshteinDistance calculates the Levenshtein distance between two strings
func levenshteinDistance(s1, s2 string) int {
len1 := len(s1)
len2 := len(s2)
// Create a 2D slice for dynamic programming
dp := make([][]int, len1+1)
for i := range dp {
dp[i] = make([]int, len2+1)
}
// Initialize first row and column
for i := 0; i <= len1; i++ {
dp[i][0] = i
}
for j := 0; j <= len2; j++ {
dp[0][j] = j
}
// Fill the dp table
for i := 1; i <= len1; i++ {
for j := 1; j <= len2; j++ {
cost := 0
if s1[i-1] != s2[j-1] {
cost = 1
}
dp[i][j] = min3(
dp[i-1][j]+1, // deletion
dp[i][j-1]+1, // insertion
dp[i-1][j-1]+cost, // substitution
)
}
}
return dp[len1][len2]
}
// ExtractKeywords extracts keywords from a string
func ExtractKeywords(text string) []string {
// Normalize and split text
text = normalizeString(text)
words := strings.Fields(text)
// Filter stopwords and short words
var keywords []string
stopwords := getStopwords()
for _, word := range words {
if len(word) > 2 && !contains(stopwords, word) {
keywords = append(keywords, word)
}
}
return keywords
}
// FindMatchedKeywords finds common keywords between two lists
func FindMatchedKeywords(keywords1, keywords2 []string) []string {
var matched []string
for _, k1 := range keywords1 {
for _, k2 := range keywords2 {
if strings.EqualFold(k1, k2) || CalculateStringSimilarity(k1, k2) > 0.8 {
if !contains(matched, k1) {
matched = append(matched, k1)
}
break
}
}
}
return matched
}
// normalizeString normalizes a string (lowercase, remove extra spaces)
func normalizeString(s string) string {
// Convert to lowercase
s = strings.ToLower(s)
// Remove punctuation and extra spaces
var result strings.Builder
for _, r := range s {
if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsSpace(r) {
result.WriteRune(r)
} else {
result.WriteRune(' ')
}
}
// Remove multiple spaces
s = strings.Join(strings.Fields(result.String()), " ")
return strings.TrimSpace(s)
}
// getStopwords returns common Indonesian stopwords
func getStopwords() []string {
return []string{
"dan", "atau", "dengan", "untuk", "dari", "ke", "di", "yang", "ini", "itu",
"ada", "adalah", "akan", "telah", "sudah", "pada", "oleh", "sebagai", "dalam",
"juga", "saya", "kamu", "dia", "kita", "mereka", "kami", "the", "a", "an",
"of", "to", "in", "for", "on", "at", "by", "with", "from",
}
}
// contains checks if a slice contains a string
func contains(slice []string, str string) bool {
for _, s := range slice {
if strings.EqualFold(s, str) {
return true
}
}
return false
}
// min3 returns the minimum of three integers
func min3(a, b, c int) int {
if a < b {
if a < c {
return a
}
return c
}
if b < c {
return b
}
return c
}
+85
View File
@@ -0,0 +1,85 @@
// internal/utils/validator.go
package utils
import (
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
var validate *validator.Validate
// InitValidator initializes the validator
func InitValidator() {
validate = validator.New()
// Register custom validators
validate.RegisterValidation("phone", validatePhone)
validate.RegisterValidation("nrp", validateNRP)
}
// ValidateStruct validates a struct
func ValidateStruct(s interface{}) error {
if validate == nil {
InitValidator()
}
return validate.Struct(s)
}
// validatePhone validates Indonesian phone numbers
func validatePhone(fl validator.FieldLevel) bool {
phone := fl.Field().String()
if phone == "" {
return true // Allow empty (use required tag separately)
}
// Remove spaces and dashes
phone = strings.ReplaceAll(phone, " ", "")
phone = strings.ReplaceAll(phone, "-", "")
// Check if starts with 0 or +62 or 62
pattern := `^(0|\+?62)[0-9]{8,12}$`
matched, _ := regexp.MatchString(pattern, phone)
return matched
}
// validateNRP validates NRP (Nomor Registrasi Pokok)
func validateNRP(fl validator.FieldLevel) bool {
nrp := fl.Field().String()
if nrp == "" {
return true // Allow empty (use required tag separately)
}
// NRP format: 10 digits
pattern := `^[0-9]{10}$`
matched, _ := regexp.MatchString(pattern, nrp)
return matched
}
// IsValidEmail checks if email is valid
func IsValidEmail(email string) bool {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, email)
return matched
}
// IsValidURL checks if URL is valid
func IsValidURL(url string) bool {
pattern := `^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$`
matched, _ := regexp.MatchString(pattern, url)
return matched
}
// SanitizeString removes potentially dangerous characters
func SanitizeString(s string) string {
// Remove control characters
s = strings.Map(func(r rune) rune {
if r < 32 && r != '\n' && r != '\r' && r != '\t' {
return -1
}
return r
}, s)
return strings.TrimSpace(s)
}