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
+23
View File
@@ -0,0 +1,23 @@
// internal/middleware/cors.go
package middleware
import (
"github.com/gin-gonic/gin"
)
// CORSMiddleware handles CORS
func CORSMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
if ctx.Request.Method == "OPTIONS" {
ctx.AbortWithStatus(204)
return
}
ctx.Next()
}
}
+169
View File
@@ -0,0 +1,169 @@
// internal/middleware/idempotency.go
package middleware
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"lost-and-found/internal/utils"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// ✅ KRITERIA BACKEND: Idempotency (5 Poin) - Advanced Feature
// IdempotencyStore menyimpan hasil request yang sudah diproses
type IdempotencyStore struct {
mu sync.RWMutex
results map[string]*IdempotencyResult
}
// IdempotencyResult menyimpan response dari request sebelumnya
type IdempotencyResult struct {
StatusCode int
Body interface{}
Timestamp time.Time
}
var idempotencyStore = &IdempotencyStore{
results: make(map[string]*IdempotencyResult),
}
// cleanupIdempotencyStore membersihkan hasil yang sudah lama (> 24 jam)
func cleanupIdempotencyStore() {
ticker := time.NewTicker(1 * time.Hour)
go func() {
for range ticker.C {
idempotencyStore.mu.Lock()
now := time.Now()
for key, result := range idempotencyStore.results {
if now.Sub(result.Timestamp) > 24*time.Hour {
delete(idempotencyStore.results, key)
}
}
idempotencyStore.mu.Unlock()
}
}()
}
func init() {
cleanupIdempotencyStore()
}
// IdempotencyMiddleware mencegah double-submit pada endpoint kritis
// Endpoint kritis: Payment, Create Order, Transfer, Submit Claim
func IdempotencyMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
// Hanya apply untuk POST/PUT/PATCH methods
if ctx.Request.Method != http.MethodPost &&
ctx.Request.Method != http.MethodPut &&
ctx.Request.Method != http.MethodPatch {
ctx.Next()
return
}
// Get idempotency key from header
idempotencyKey := ctx.GetHeader("Idempotency-Key")
// Jika tidak ada idempotency key, skip (tidak wajib untuk semua request)
if idempotencyKey == "" {
ctx.Next()
return
}
// Generate unique key: idempotency-key + user-id + path + method
userID, _ := ctx.Get("user_id")
uniqueKey := generateUniqueKey(idempotencyKey, fmt.Sprintf("%v", userID), ctx.Request.URL.Path, ctx.Request.Method)
// Check if request sudah pernah diproses
idempotencyStore.mu.RLock()
result, exists := idempotencyStore.results[uniqueKey]
idempotencyStore.mu.RUnlock()
if exists {
// Request sudah pernah diproses, return hasil sebelumnya
ctx.JSON(result.StatusCode, gin.H{
"success": true,
"message": "Request already processed (idempotent)",
"data": result.Body,
"idempotent": true,
"original_at": result.Timestamp,
})
ctx.Abort()
return
}
// Mark request sebagai "processing" untuk prevent concurrent duplicate
processingLock := fmt.Sprintf("%s-processing", uniqueKey)
idempotencyStore.mu.Lock()
if _, processing := idempotencyStore.results[processingLock]; processing {
idempotencyStore.mu.Unlock()
utils.ErrorResponse(ctx, http.StatusConflict, "Request is being processed", "Duplicate request detected")
ctx.Abort()
return
}
// Lock dengan timestamp sebagai marker
idempotencyStore.results[processingLock] = &IdempotencyResult{
Timestamp: time.Now(),
}
idempotencyStore.mu.Unlock()
// Custom response writer untuk capture hasil
blw := &bodyLogWriter{body: []byte{}, ResponseWriter: ctx.Writer}
ctx.Writer = blw
// Process request
ctx.Next()
// Remove processing lock
idempotencyStore.mu.Lock()
delete(idempotencyStore.results, processingLock)
idempotencyStore.mu.Unlock()
// Simpan hasil hanya jika sukses (2xx status)
if ctx.Writer.Status() >= 200 && ctx.Writer.Status() < 300 {
idempotencyStore.mu.Lock()
idempotencyStore.results[uniqueKey] = &IdempotencyResult{
StatusCode: ctx.Writer.Status(),
Body: blw.body,
Timestamp: time.Now(),
}
idempotencyStore.mu.Unlock()
}
}
}
// bodyLogWriter untuk capture response body
type bodyLogWriter struct {
gin.ResponseWriter
body []byte
}
func (w *bodyLogWriter) Write(b []byte) (int, error) {
w.body = append(w.body, b...)
return w.ResponseWriter.Write(b)
}
// generateUniqueKey membuat unique key dari kombinasi parameter
func generateUniqueKey(idempotencyKey, userID, path, method string) string {
data := fmt.Sprintf("%s:%s:%s:%s", idempotencyKey, userID, path, method)
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
// ClearIdempotencyCache membersihkan cache (untuk testing/admin)
func ClearIdempotencyCache() {
idempotencyStore.mu.Lock()
defer idempotencyStore.mu.Unlock()
idempotencyStore.results = make(map[string]*IdempotencyResult)
}
// GetIdempotencyCacheSize return ukuran cache (untuk monitoring)
func GetIdempotencyCacheSize() int {
idempotencyStore.mu.RLock()
defer idempotencyStore.mu.RUnlock()
return len(idempotencyStore.results)
}
+106
View File
@@ -0,0 +1,106 @@
// internal/middleware/jwt_middleware.go
package middleware
import (
"lost-and-found/internal/config"
"lost-and-found/internal/repositories"
"lost-and-found/internal/utils"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// JWTMiddleware validates JWT token
func JWTMiddleware(db *gorm.DB) gin.HandlerFunc {
return func(ctx *gin.Context) {
// Get token from Authorization header
authHeader := ctx.GetHeader("Authorization")
if authHeader == "" {
utils.ErrorResponse(ctx, http.StatusUnauthorized, "Authorization header required", "")
ctx.Abort()
return
}
// Check if Bearer token
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
utils.ErrorResponse(ctx, http.StatusUnauthorized, "Invalid authorization format", "")
ctx.Abort()
return
}
tokenString := parts[1]
// Validate token
claims, err := config.ValidateToken(tokenString)
if err != nil {
utils.ErrorResponse(ctx, http.StatusUnauthorized, "Invalid or expired token", err.Error())
ctx.Abort()
return
}
// Get user from database
userRepo := repositories.NewUserRepository(db)
user, err := userRepo.FindByID(claims.UserID)
if err != nil {
utils.ErrorResponse(ctx, http.StatusUnauthorized, "User not found", "")
ctx.Abort()
return
}
// Check if user is blocked
if user.IsBlocked() {
utils.ErrorResponse(ctx, http.StatusForbidden, "Account is blocked", "")
ctx.Abort()
return
}
// Set user in context
ctx.Set("user", user)
ctx.Set("user_id", user.ID)
ctx.Set("user_role", user.Role.Name)
ctx.Next()
}
}
// OptionalJWTMiddleware validates JWT token if present (for public routes that can benefit from auth)
func OptionalJWTMiddleware(db *gorm.DB) gin.HandlerFunc {
return func(ctx *gin.Context) {
authHeader := ctx.GetHeader("Authorization")
if authHeader == "" {
ctx.Next()
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
ctx.Next()
return
}
tokenString := parts[1]
claims, err := config.ValidateToken(tokenString)
if err != nil {
ctx.Next()
return
}
userRepo := repositories.NewUserRepository(db)
user, err := userRepo.FindByID(claims.UserID)
if err != nil {
ctx.Next()
return
}
if !user.IsBlocked() {
ctx.Set("user", user)
ctx.Set("user_id", user.ID)
ctx.Set("user_role", user.Role.Name)
}
ctx.Next()
}
}
+46
View File
@@ -0,0 +1,46 @@
// internal/middleware/logger.go
package middleware
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// LoggerMiddleware logs HTTP requests
func LoggerMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
// Start timer
startTime := time.Now()
// Process request
ctx.Next()
// Calculate latency
latency := time.Since(startTime)
// Get request info
method := ctx.Request.Method
path := ctx.Request.URL.Path
statusCode := ctx.Writer.Status()
clientIP := ctx.ClientIP()
// Get user ID if authenticated
userID := "guest"
if id, exists := ctx.Get("user_id"); exists {
userID = fmt.Sprintf("%v", id)
}
// Log format
fmt.Printf("[%s] %s | %3d | %13v | %15s | %s | User: %s\n",
time.Now().Format("2006-01-02 15:04:05"),
method,
statusCode,
latency,
clientIP,
path,
userID,
)
}
}
+113
View File
@@ -0,0 +1,113 @@
// internal/middleware/rate_limiter.go
package middleware
import (
"lost-and-found/internal/utils"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// RateLimiter stores rate limit data
type RateLimiter struct {
visitors map[string]*Visitor
mu sync.RWMutex
rate int // requests per window
window time.Duration // time window
}
// Visitor represents a visitor's rate limit data
type Visitor struct {
lastSeen time.Time
count int
}
var limiter *RateLimiter
// InitRateLimiter initializes the rate limiter
func InitRateLimiter(rate int, window time.Duration) {
limiter = &RateLimiter{
visitors: make(map[string]*Visitor),
rate: rate,
window: window,
}
// Cleanup old visitors every minute
go limiter.cleanupVisitors()
}
// cleanupVisitors removes old visitor entries
func (rl *RateLimiter) cleanupVisitors() {
for {
time.Sleep(time.Minute)
rl.mu.Lock()
for ip, visitor := range rl.visitors {
if time.Since(visitor.lastSeen) > rl.window {
delete(rl.visitors, ip)
}
}
rl.mu.Unlock()
}
}
// getVisitor retrieves or creates a visitor
func (rl *RateLimiter) getVisitor(ip string) *Visitor {
rl.mu.Lock()
defer rl.mu.Unlock()
visitor, exists := rl.visitors[ip]
if !exists {
visitor = &Visitor{
lastSeen: time.Now(),
count: 0,
}
rl.visitors[ip] = visitor
}
return visitor
}
// isAllowed checks if request is allowed
func (rl *RateLimiter) isAllowed(ip string) bool {
visitor := rl.getVisitor(ip)
rl.mu.Lock()
defer rl.mu.Unlock()
// Reset count if window has passed
if time.Since(visitor.lastSeen) > rl.window {
visitor.count = 0
visitor.lastSeen = time.Now()
}
// Check if limit exceeded
if visitor.count >= rl.rate {
return false
}
visitor.count++
visitor.lastSeen = time.Now()
return true
}
// RateLimiterMiddleware applies rate limiting
func RateLimiterMiddleware() gin.HandlerFunc {
// Initialize rate limiter (1000 requests per minute)
if limiter == nil {
InitRateLimiter(1000, time.Minute)
}
return func(ctx *gin.Context) {
ip := ctx.ClientIP()
if !limiter.isAllowed(ip) {
utils.ErrorResponse(ctx, http.StatusTooManyRequests, "Rate limit exceeded", "Too many requests, please try again later")
ctx.Abort()
return
}
ctx.Next()
}
}
+81
View File
@@ -0,0 +1,81 @@
// internal/middleware/role_middleware.go
package middleware
import (
"lost-and-found/internal/models"
"lost-and-found/internal/utils"
"net/http"
"github.com/gin-gonic/gin"
)
func RequirePermission(requiredPerm string) gin.HandlerFunc {
return func(ctx *gin.Context) {
// 1. Ambil object user dari context (diset oleh JWTMiddleware)
userObj, exists := ctx.Get("user")
if !exists {
utils.ErrorResponse(ctx, http.StatusUnauthorized, "Authentication required", "")
ctx.Abort()
return
}
user := userObj.(*models.User)
// 2. Cek Permission menggunakan method helper di model User
// Pastikan method HasPermission sudah ditambahkan di internal/models/user.go
if !user.HasPermission(requiredPerm) {
utils.ErrorResponse(ctx, http.StatusForbidden, "Insufficient permissions", "Missing permission: "+requiredPerm)
ctx.Abort()
return
}
ctx.Next()
}
}
// RequireRole checks if user has required role
func RequireRole(allowedRoles ...string) gin.HandlerFunc {
return func(ctx *gin.Context) {
userObj, exists := ctx.Get("user")
if !exists {
utils.ErrorResponse(ctx, http.StatusUnauthorized, "Authentication required", "")
ctx.Abort()
return
}
user := userObj.(*models.User)
userRole := user.Role.Name
// Check if user has allowed role
hasRole := false
for _, role := range allowedRoles {
if userRole == role {
hasRole = true
break
}
}
if !hasRole {
utils.ErrorResponse(ctx, http.StatusForbidden, "Insufficient permissions", "")
ctx.Abort()
return
}
ctx.Next()
}
}
// RequireAdmin middleware (admin only)
func RequireAdmin() gin.HandlerFunc {
return RequireRole(models.RoleAdmin)
}
// RequireManager middleware (manager and admin)
func RequireManager() gin.HandlerFunc {
return RequireRole(models.RoleAdmin, models.RoleManager)
}
// RequireUser middleware (all authenticated users)
func RequireUser() gin.HandlerFunc {
return RequireRole(models.RoleAdmin, models.RoleManager, models.RoleUser)
}