diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml new file mode 100644 index 0000000000000000000000000000000000000000..4ae0a513c7d1a4e20dd9c146de1796f30af7066a --- /dev/null +++ b/.github/workflows/deploy-development.yml @@ -0,0 +1,32 @@ + +name: Deploy to Development +on: + push: + branches: + - development + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add VPS to known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts + + - name: Push to VPS + run: | + git remote remove vps || true + git remote add vps git@${{ secrets.VPS_HOST }}:/home/git/repos/backend-development.git + git push vps HEAD:development --force diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000000000000000000000000000000000000..82c704b9868c8e511799507185feb82c33b58887 --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,32 @@ +name: Deploy to Production + +on: + push: + branches: + - production + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add VPS to known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts + + - name: Push to VPS + run: | + git remote remove vps || true + git remote add vps git@${{ secrets.VPS_HOST }}:/home/git/repos/backend-production.git + git push vps HEAD:production --force \ No newline at end of file diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..6df6f63d6dc91cce775771ac14c0c09bc07b62c8 --- /dev/null +++ b/.github/workflows/deploy-test.yml @@ -0,0 +1,49 @@ +name: Deploy to Huggingface - tests +on: + push: + branches: + - test +jobs: + deploy-to-huggingface: + runs-on: ubuntu-latest + steps: + # Checkout repository + - name: Checkout Repository + uses: actions/checkout@v3 + # Setup Git + - name: Setup Git for Huggingface + run: | + git config --global user.email "abdan.hafidz@gmail.com" + git config --global user.name "abdanhafidz" + # Clone Huggingface Space Repository + - name: Clone Huggingface Space + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + git clone https://huggingface.co/spaces/lifedebugger/DanzApp-BE-Test space + # Update Git Remote URL and Pull Latest Changes + - name: Update Remote and Pull Changes + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + cd space + git remote set-url origin https://lifedebugger:$HF_TOKEN@huggingface.co/spaces/lifedebugger/DanzApp-BE-Test + git pull origin main || echo "No changes to pull" + # Clean Space Directory - Delete all files except .git + - name: Clean Space Directory + run: | + cd space + find . -mindepth 1 -not -path "./.git*" -delete + # Copy Files to Huggingface Space + - name: Copy Files to Space + run: | + rsync -av --exclude='.git' ./ space/ + # Commit and Push to Huggingface Space + - name: Commit and Push to Huggingface + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + cd space + git add . + git commit -m "Deploy files from GitHub repository" || echo "No changes to commit" + git push origin main || echo "No changes to push" \ No newline at end of file diff --git a/.github/workflows/go-build-test.yml b/.github/workflows/go-build-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..7ed4753f5eacb369f692a8473c290a63b1587da1 --- /dev/null +++ b/.github/workflows/go-build-test.yml @@ -0,0 +1,28 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "test" ] + pull_request: + branches: [ "development" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... \ No newline at end of file diff --git a/.github/workflows/uptime-check.yml b/.github/workflows/uptime-check.yml new file mode 100644 index 0000000000000000000000000000000000000000..051bd2b49d702b3c904aa807aea1851414ffa627 --- /dev/null +++ b/.github/workflows/uptime-check.yml @@ -0,0 +1,44 @@ + +name: Uptime Check Backend API + +on: + push: + branches: + - tests + +jobs: + check-uptime: + runs-on: ubuntu-latest + + steps: + - name: Wait 5 Minutes Before Checking Uptime + run: | + echo "⏳ Waiting 5 minutes before running uptime checks..." + sleep 300 + + - name: Check Dev API Uptime + run: | + echo "🔍 Checking Development API..." + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" {YOUR_DEV_ENDPOINT}) + + echo "Development API HTTP Status: $RESPONSE" + + if [ "$RESPONSE" -ne 200 ]; then + echo "❌ Development API DOWN! Expected 200 but got $RESPONSE" + exit 1 + fi + + - name: Check Test API Uptime + run: | + echo "🔍 Checking Test API..." + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" {YOUR_TEST_ENDPOINT}) + + echo "Test API HTTP Status: $RESPONSE" + + if [ "$RESPONSE" -ne 200 ]; then + echo "❌ Test API DOWN! Expected 200 but got $RESPONSE" + exit 1 + fi + + - name: Success Message + run: echo "✔ All API endpoints are UP!" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4d1e9c8ec810da353f6d05c40a94056acc50cdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.env +go-boilerplate.exe +vendor/ +*.log +*.exe +*.dll +*.so +*.dylib \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6844b0ff8a6472fc0f96949182dcbba96e912c52 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# ===================== +# BUILD STAGE +# ===================== +FROM golang:1.25.4 AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app + +# ===================== +# RUNTIME STAGE +# ===================== +FROM gcr.io/distroless/base-debian12 + +WORKDIR /app + +COPY --from=builder /app/app /app/app + +# Non-root user +USER nonroot:nonroot + +EXPOSE 8080 + +CMD ["/app/app"] diff --git a/README.md b/README.md index 1c941e50038e31cc7c871abde215093543b41297..0cdf58e6d876c13d228ca1687e61bf25fea9712e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,216 @@ ---- -title: DanzApp BE Test -emoji: 🏆 -colorFrom: yellow -colorTo: red -sdk: docker -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# 🚀 Go Starter Boilerplate - Clean Layered Architecture + +[![Go Version](https://img.shields.io/badge/Go-1.25.4+-00ADD8?style=flat&logo=go)](https://golang.org/) +[![Gin Framework](https://img.shields.io/badge/Framework-Gin-008080?style=flat)](https://gin-gonic.com/) +[![GORM](https://img.shields.io/badge/ORM-GORM-blue?style=flat)](https://gorm.io/) +[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) + +A high-performance, scalable, and professional Go boilerplate built with **Clean Layered Architecture**. This starter kit is designed to provide a solid foundation for enterprise-grade backend applications, featuring dependency injection, modularity, and robust CI/CD pipelines. + +--- + +## 📑 Table of Contents +- [Core Architecture & Theory](#-core-architecture--theory) +- [Tech Stack](#-tech-stack) +- [Key Features](#-key-features) +- [Project Structure](#-project-structure) +- [CLI Tools & Automated DI](#-cli-tools--automated-di) +- [Setup & Installation](#-setup--installation) +- [Environment Variables](#-environment-variables) +- [Documentation (Swagger)](#-documentation-swagger) +- [CI/CD & Deployment](#-cicd--deployment) +- [SOLID Principles](#-solid-principles) + +--- + +## 🏗️ Core Architecture & Theory + +This project follows the **Clean Layered Architecture** pattern, ensuring high maintainability and testability by decoupling business logic from external dependencies (DB, API, Frameworks). + +### The Four Layers: +1. **Transport / Router**: Defines API endpoints and handles HTTP protocol specifics using Gin. +2. **Controller**: Entry point for requests. Validates input, calls services, and formats JSON responses. +3. **Service (Usecase)**: The heart of the application. Contains business logic and orchestrates data flow between repositories. +4. **Repository (Data Access)**: Handles database interactions using GORM. Isolated from business logic. + +### 🔌 Dependency Injection (Provider Pattern) +We use a **Provider Pattern** for Dependency Injection, managed through the `provider/` directory. While the wiring is handled in Go, this project features an **Automated DI Engine** that generates this wiring for you. + +Benefits: +- **Singleton Management**: Ensures single instances of services/repositories. +- **Automated Wiring**: No more manual `NewService(repo1, repo2, ...)` maintenance. +- **Cycle Detection**: The induction engine automatically detects and prevents circular dependencies. +- **Decoupling**: Layers remain isolated through interfaces. + +--- + +## 🛠️ Tech Stack + +- **Language**: [Go (Golang)](https://golang.org/) +- **Web Framework**: [Gin Gonic](https://github.com/gin-gonic/gin) +- **ORM**: [GORM](https://gorm.io/) with PostgreSQL +- **Database**: [PostgreSQL](https://www.postgresql.org/) +- **Authentication**: [JWT (JSON Web Token)](https://jwt.io/) +- **Payment Gateway**: [Xendit](https://www.xendit.co/) +- **Object Storage**: [Supabase Storage](https://supabase.com/storage) +- **Documentation**: [Swagger (swaggo)](https://github.com/swaggo/swag) +- **Containerization**: [Docker](https://www.docker.com/) + +--- + +## ✨ Key Features + +- **Lock-tight Auth**: Complete Authentication flow (Login, Register, Email Verification, Forgot Password). +- **Role-Based Access**: Specialized routes for Admin and User roles. +- **Payment Ready**: Integrated with Xendit for seamless payment processing and webhooks. +- **Cloud Storage**: Native support for Supabase storage for file uploads. +- **Automated Migrations**: Database schema automatically synchronizes on startup. +- **Standardized Responses**: Unified JSON response structure for success and error handling. +- **Modular Routing**: Cleanly separated route definitions per module. + +--- + +## 📁 Project Structure + +```text +├── cmd/ # Custom CLI tools (e.g., Swagger compiler) +├── config/ # Configuration loaders & schema definitions +├── controllers/ # Request handlers (Input validation, Response formatting) +├── middleware/ # Gin middlewares (Auth, Logging, Gzip) +├── models/ +│ ├── dto/ # Data Transfer Objects (Request/Response structs) +│ ├── entity/ # GORM database models +│ └── error/ # Custom error definitions +├── provider/ # Dependency Injection & Bootstrapping +├── repositories/ # Database access layer (SQL logic) +├── router/ # Route definitions & grouping +├── services/ # Business logic layer +├── swagger/ # Automatically generated Swagger UI files +├── utils/ # Cross-cutting helpers (Response helpers, string utils) +├── main.go # Application entry point +├── Dockerfile # Multi-stage production container +└── .github/workflows/ # CI/CD Pipelines +``` + +--- + +## 🛠️ CLI Tools & Automated DI + +This boilerplate features a unique **PowerShell-based Dependency Injection Engine** located in the `cmd/` directory. These scripts scan your code, resolve dependencies, and automatically generate the necessary boilerplate code in the `provider/` package. + +### 💉 Automated Injection Scripts +| Script | Purpose | +| :--- | :--- | +| `do_inject_config.ps1` | Scans configuration files and wires them into the Config Provider. | +| `do_inject_repository.ps1` | Discovers GORM repositories and updates `repositories_provider.go`. | +| `do_inject_services.ps1` | Performs **topological sorting** on services to resolve their dependencies in the correct order. | +| `do_inject_controllers.ps1` | Wires services into controllers and updates the Controller Provider. | +| `do_inject_middleware.ps1` | Manages registration of custom Gin middlewares. | + +### 🧠 How the DI Engine Works: +1. **Scanning**: The scripts use regex to find constructor functions (e.g., `NewAccountService`). +2. **Resolution**: It identifies the required parameters (Repositories, other Services, or Configs). +3. **Topological Sort**: (For Services) It builds a dependency graph and sorts them so dependencies are initialized before they are needed. +4. **Codegen**: It writes a clean, standardized `provider/*.go` file with all the wiring logic. + +### 📝 Swagger Compilation +- **`compile_swagger.ps1`**: This script wraps the `swag init` command with optimized flags (`--parseDependency`, `--parseInternal`) to ensure your Swagger documentation is always complete and up-to-date. + +--- + +## 🚀 Setup & Installation + +### Prerequisites +- Go 1.25.4 or higher +- PostgreSQL +- Docker (Optional, for containerization) + +### Step 1: Clone the Repository +```bash +git clone +cd +``` + +### Step 2: Environment Setup +Copy the example environment file and fill in your credentials: +```bash +cp .env.example .env +``` + +### Step 3: Install Dependencies +```bash +go mod download +``` + +### Step 4: Run the Application +```bash +go run main.go +``` + +--- + +## � Environment Variables + +| Variable | Description | +| :--- | :--- | +| `DB_HOST` | PostgreSQL Host address | +| `DB_USER` | Database username | +| `DB_PASSWORD` | Database password | +| `DB_PORT` | Database port (default 5432) | +| `DB_NAME` | Name of the database | +| `JWT_SECRET_KEY` | Secret key for signing JWT tokens | +| `XENDIT_API_KEY` | Your Xendit Secret Key | +| `HOST_PORT` | Port for the Go server to listen on | + +--- + +## 📖 Documentation (Swagger) + +This project uses `swaggo` to generate interactive API documentation. + +### How to Compile Swagger: +Run the provided script to synchronize your code comments with the documentation: +```bash +./cmd/compile_swagger +``` + +### Accessing Swagger UI: +Once the app is running, visit: +`http://localhost:/swagger/index.html` + +--- + +## 🚢 CI/CD & Deployment + +### GitHub Workflows +The project includes automated pipelines in `.github/workflows/`: +- **`go-build-test.yml`**: Automatically runs tests and builds the binary on every push. +- **`deploy-production.yml` / `deploy-development.yml`**: CD pipelines for automated shipping to target environments. +- **`uptime-check.yml`**: Specialized workflow to monitor service health. + +### Docker +We use a **Multi-Stage Dockerfile** to ensure the production image is as small and secure as possible. + +**Build the image:** +```bash +docker build -t go-starter . +``` + +**Run the container:** +```bash +docker run -p 8080:8080 --env-file .env go-starter +``` + +--- + +## 💎 SOLID Principles + +- **S**: Single Responsibility. Each layer (Controller, Service, Repo) does exactly one thing. +- **O**: Open/Closed. Services are open for extension via interfaces but closed for modification. +- **L**: Liskov Substitution. Implementations are interchangeable via Provider interfaces. +- **I**: Interface Segregation. Slim, specific interfaces for each capability. +- **D**: Dependency Inversion. High-level modules don't depend on low-level modules; both depend on abstractions. + +--- + +Developed with ❤️ by Abdan Hafidz. \ No newline at end of file diff --git a/cmd/compile_swagger.ps1 b/cmd/compile_swagger.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..d7c4d137def8e8833fabfda7ea34a75e0d413ae8 --- /dev/null +++ b/cmd/compile_swagger.ps1 @@ -0,0 +1 @@ +swag init -g swagger/swagger_info.go -o swagger/docs --parseDependency --parseInternal \ No newline at end of file diff --git a/cmd/do_inject_config.ps1 b/cmd/do_inject_config.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..521b1192e1ede56c460022711ed977a356c17ae3 --- /dev/null +++ b/cmd/do_inject_config.ps1 @@ -0,0 +1,435 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Automatic Dependency Injection Generator for Go Configuration + +.DESCRIPTION + Scans ./config/ directory, discovers all config constructors, infers their dependencies, + and generates provider/config_provider.go with full DI wiring. Special handling for + chained dependencies where configs depend on other configs (e.g., DatabaseConfig depends on EnvConfig). + +.EXAMPLE + .\config_injector.ps1 + +.NOTES + - Works with PowerShell 5.1+ and PowerShell 7+ + - No external dependencies required + - Supports multi-line constructor signatures + - Handles config-to-config dependencies with topological sorting + - Special handling for method calls like envConfig.GetDatabaseHost() +#> + +[CmdletBinding()] +param() + +# Configuration +$ConfigDir = "./config" +$OutputFile = "provider/config_provider.go" +$ModulePath = "abdanhafidz.com/go-boilerplate/config" + +# ANSI colors for better output +$script:UseColors = $Host.UI.SupportsVirtualTerminal +function Write-ColorOutput { + param([string]$Message, [string]$Color = "White") + if ($script:UseColors) { + $colors = @{ + "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m" + "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m" + } + Write-Host "$($colors[$Color])$Message$($colors['Reset'])" + } else { + Write-Host $Message + } +} + +# Data structures +class ConfigInfo { + [string]$ConstructorName # NewDatabaseConfig + [string]$Domain # DatabaseConfig + [string]$VarName # databaseConfig + [System.Collections.Generic.List[Parameter]]$Parameters + [System.Collections.Generic.List[string]]$ConfigDependencies + + ConfigInfo() { + $this.Parameters = [System.Collections.Generic.List[Parameter]]::new() + $this.ConfigDependencies = [System.Collections.Generic.List[string]]::new() + } +} + +class Parameter { + [string]$Name + [string]$RawType + [string]$NormalizedType +} + +function Get-LowerCamelCase { + param([string]$Text) + if ($Text.Length -eq 0) { return $Text } + return $Text.Substring(0, 1).ToLower() + $Text.Substring(1) +} + +function Normalize-TypeName { + param([string]$TypeStr) + + # Remove leading pointer + $cleaned = $TypeStr -replace '^\*+', '' + + # Remove package prefix (everything before last dot) + if ($cleaned -match '\.([^.]+)$') { + $cleaned = $matches[1] + } + + return $cleaned.Trim() +} + +function Parse-GoFiles { + param([string]$Directory) + + Write-ColorOutput "Scanning for config constructors in $Directory..." "Cyan" + + if (-not (Test-Path $Directory)) { + Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red" + exit 1 + } + + $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File + $configs = [System.Collections.Generic.List[ConfigInfo]]::new() + + foreach ($file in $goFiles) { + $content = Get-Content $file.FullName -Raw + + # Match function signatures (support multi-line) + # Pattern: func NewXxxConfig(...) XxxConfig + $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Config)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Config)' + $matches = [regex]::Matches($content, $pattern) + + foreach ($match in $matches) { + $constructorName = $match.Groups[1].Value + $paramsStr = $match.Groups[2].Value + $returnType = $match.Groups[3].Value + + # Extract domain name (XxxConfig) + $domain = Normalize-TypeName $returnType + $varName = Get-LowerCamelCase $domain + + $config = [ConfigInfo]::new() + $config.ConstructorName = $constructorName + $config.Domain = $domain + $config.VarName = $varName + + # Parse parameters + if ($paramsStr.Trim() -ne "") { + # Split by comma, but be careful with nested types + $paramList = $paramsStr -split ',\s*(?![^<>]*>)' + + foreach ($param in $paramList) { + $param = $param.Trim() + if ($param -eq "") { continue } + + # Split into name and type + $parts = $param -split '\s+', 2 + + $p = [Parameter]::new() + if ($parts.Count -eq 2) { + $p.Name = $parts[0] + $p.RawType = $parts[1] + } elseif ($parts.Count -eq 1) { + # Anonymous parameter - synthesize name + $p.Name = "param$($config.Parameters.Count)" + $p.RawType = $parts[0] + } else { + continue + } + + $p.NormalizedType = Normalize-TypeName $p.RawType + $config.Parameters.Add($p) + + # Track config dependencies (configs that depend on other configs) + if ($p.NormalizedType -match 'Config$') { + $config.ConfigDependencies.Add($p.NormalizedType) + } + } + } + + $configs.Add($config) + Write-ColorOutput " Found: $constructorName" "Green" + } + } + + if ($configs.Count -eq 0) { + Write-ColorOutput "No config constructors found matching pattern 'NewXxxConfig'!" "Red" + exit 1 + } + + Write-ColorOutput "`nTotal configs discovered: $($configs.Count)" "Blue" + return $configs +} + +function Get-TopologicalOrder { + param([System.Collections.Generic.List[ConfigInfo]]$Configs) + + Write-ColorOutput "`nBuilding dependency graph..." "Cyan" + + # Build adjacency list + $graph = @{} + $inDegree = @{} + $domainToConfig = @{} + + foreach ($cfg in $Configs) { + $graph[$cfg.Domain] = [System.Collections.Generic.List[string]]::new() + $inDegree[$cfg.Domain] = 0 + $domainToConfig[$cfg.Domain] = $cfg + } + + # Build edges (dependencies) + foreach ($cfg in $Configs) { + foreach ($dep in $cfg.ConfigDependencies) { + if ($graph.ContainsKey($dep)) { + $graph[$dep].Add($cfg.Domain) + $inDegree[$cfg.Domain]++ + } + } + } + + # Kahn's algorithm for topological sort + $queue = [System.Collections.Generic.Queue[string]]::new() + foreach ($domain in $inDegree.Keys) { + if ($inDegree[$domain] -eq 0) { + $queue.Enqueue($domain) + } + } + + $sorted = [System.Collections.Generic.List[string]]::new() + + while ($queue.Count -gt 0) { + $current = $queue.Dequeue() + $sorted.Add($current) + + foreach ($neighbor in $graph[$current]) { + $inDegree[$neighbor]-- + if ($inDegree[$neighbor] -eq 0) { + $queue.Enqueue($neighbor) + } + } + } + + # Check for cycles + if ($sorted.Count -ne $Configs.Count) { + $remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 } + Write-ColorOutput "`nERROR: Circular dependency detected in configs!" "Red" + Write-ColorOutput "Configs involved in cycle: $($remaining -join ', ')" "Yellow" + exit 1 + } + + Write-ColorOutput " Dependency graph validated (no cycles)" "Green" + Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue" + + # Return configs in topological order + return $sorted | ForEach-Object { $domainToConfig[$_] } +} + +function Resolve-ConfigArgument { + param( + [Parameter]$Param, + [string]$DependentConfigVar + ) + + $type = $Param.NormalizedType + $paramName = $Param.Name + + # SPECIAL CASE MAPPINGS FOR CONFIG + # ============================================ + + # 1. Config pattern: XxxConfig -> already instantiated config variable + if ($type -match '^(.+)Config$') { + $configVarName = Get-LowerCamelCase $type + return $configVarName + } + + # 2. String parameters - try to infer from parameter name and match with config getter methods + if ($type -eq "string") { + # Common patterns for EnvConfig getters + $getterMappings = @{ + "host" = "GetDatabaseHost()" + "databaseHost" = "GetDatabaseHost()" + "user" = "GetDatabaseUser()" + "databaseUser" = "GetDatabaseUser()" + "password" = "GetDatabasePassword()" + "databasePassword" = "GetDatabasePassword()" + "name" = "GetDatabaseName()" + "databaseName" = "GetDatabaseName()" + "dbName" = "GetDatabaseName()" + "port" = "GetDatabasePort()" + "databasePort" = "GetDatabasePort()" + "salt" = "GetSalt()" + "secret" = "GetSecretKey()" + "secretKey" = "GetSecretKey()" + "jwtSecret" = "GetSecretKey()" + "apiKey" = "GetAPIKey()" + "timezone" = "GetTimezone()" + } + + # Try to find matching getter + foreach ($key in $getterMappings.Keys) { + if ($paramName -like "*$key*") { + return "envConfig.$($getterMappings[$key])" + } + } + + # If dependent on a config, try to construct getter name from param name + if ($DependentConfigVar) { + # Convert paramName to PascalCase for getter + $getterName = (Get-Culture).TextInfo.ToTitleCase($paramName) + $getterName = $getterName -replace '\s', '' + return "${DependentConfigVar}.Get${getterName}()" + } + } + + # 3. Int/port parameters + if ($type -eq "int" -or $type -eq "int32" -or $type -eq "int64") { + if ($paramName -match "port") { + return "envConfig.GetDatabasePort()" + } + } + + # ADD MORE SPECIAL CASES HERE: + # -------------------------------------------- + # Example: Redis config + # if ($paramName -match "redis") { + # return "envConfig.GetRedisURL()" + # } + # + # Example: Mail config + # if ($paramName -match "smtp") { + # return "envConfig.GetSMTPHost()" + # } + # -------------------------------------------- + + # 4. Hardcoded constants (timezone example) + if ($type -eq "string" -and $paramName -match "timezone|location") { + return "`"Asia/Jakarta`"" + } + + # 5. Fallback: unresolved type + return "/* TODO: provide $($Param.RawType) for $paramName */" +} + +function Generate-ProviderCode { + param([System.Collections.Generic.List[ConfigInfo]]$ConfigsInOrder) + + Write-ColorOutput "`nGenerating config provider code..." "Cyan" + + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("package provider") + [void]$sb.AppendLine() + [void]$sb.AppendLine("import `"$ModulePath`"") + [void]$sb.AppendLine() + + # Interface + [void]$sb.AppendLine("type ConfigProvider interface {") + foreach ($cfg in $ConfigsInOrder) { + $line = "`tProvide$($cfg.Domain)() config.$($cfg.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Struct + [void]$sb.AppendLine("type configProvider struct {") + foreach ($cfg in $ConfigsInOrder) { + $line = "`t$($cfg.VarName) config.$($cfg.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Constructor + [void]$sb.AppendLine("func NewConfigProvider() ConfigProvider {") + + # Initialize configs in topological order + foreach ($cfg in $ConfigsInOrder) { + # Check if this config depends on another config + $dependentConfigVar = $null + if ($cfg.ConfigDependencies.Count -gt 0) { + $dependentConfigVar = Get-LowerCamelCase $cfg.ConfigDependencies[0] + } + + $args = @() + foreach ($param in $cfg.Parameters) { + $args += Resolve-ConfigArgument -Param $param -DependentConfigVar $dependentConfigVar + } + $argsStr = $args -join ", " + $line = "`t$($cfg.VarName) := config.$($cfg.ConstructorName)($argsStr)" + [void]$sb.AppendLine($line) + } + + [void]$sb.AppendLine("`treturn &configProvider{") + foreach ($cfg in $ConfigsInOrder) { + $line = "`t`t$($cfg.VarName): $($cfg.VarName)," + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("`t}") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Getter methods + foreach ($cfg in $ConfigsInOrder) { + [void]$sb.AppendLine("func (c *configProvider) Provide$($cfg.Domain)() config.$($cfg.Domain) {") + [void]$sb.AppendLine("`treturn c.$($cfg.VarName)") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + } + + return $sb.ToString() +} + +function Write-ProviderFile { + param([string]$Code, [string]$OutputPath) + + Write-ColorOutput "Writing to $OutputPath..." "Cyan" + + # Ensure directory exists + $dir = Split-Path $OutputPath -Parent + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + + # Write file as UTF-8 without BOM + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom) + + Write-ColorOutput " Successfully generated $OutputPath" "Green" +} + +# ============================================ +# MAIN EXECUTION +# ============================================ + +try { + Write-ColorOutput "`n=========================================" "Blue" + Write-ColorOutput " Go Config Provider Generator v1.0" "Blue" + Write-ColorOutput "=========================================`n" "Blue" + + # Step 1: Parse all config constructors + $configs = Parse-GoFiles -Directory $ConfigDir + + # Step 2: Perform topological sort (configs can depend on other configs) + $sortedConfigs = Get-TopologicalOrder -Configs $configs + + # Step 3: Generate provider code + $code = Generate-ProviderCode -ConfigsInOrder $sortedConfigs + + # Step 4: Write to file + Write-ProviderFile -Code $code -OutputPath $OutputFile + + Write-ColorOutput "`nSUCCESS! Config provider generated successfully.`n" "Green" + Write-ColorOutput "Next steps:" "Cyan" + Write-ColorOutput " 1. Review $OutputFile" "White" + Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White" + Write-ColorOutput " 3. Run: go build ./provider" "White" + +} catch { + Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red" + Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow" + exit 1 +} \ No newline at end of file diff --git a/cmd/do_inject_controllers.ps1 b/cmd/do_inject_controllers.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..74b78796905c9d0bc3d343f531c6ce2fd91003bb --- /dev/null +++ b/cmd/do_inject_controllers.ps1 @@ -0,0 +1,310 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Automatic Dependency Injection Generator for Go Controllers + +.DESCRIPTION + Scans ./controllers/ directory, discovers all controller constructors, infers their dependencies, + and generates provider/controller_provider.go with full DI wiring. + +.EXAMPLE + .\controller_injector.ps1 + +.NOTES + - Works with PowerShell 5.1+ and PowerShell 7+ + - No external dependencies required + - Supports multi-line constructor signatures + - Controllers depend on services from ServicesProvider +#> + +[CmdletBinding()] +param() + +# Configuration +$ControllersDir = "./controllers" +$OutputFile = "provider/controller_provider.go" +$ModulePath = "abdanhafidz.com/go-boilerplate/controllers" + +# ANSI colors for better output +$script:UseColors = $Host.UI.SupportsVirtualTerminal +function Write-ColorOutput { + param([string]$Message, [string]$Color = "White") + if ($script:UseColors) { + $colors = @{ + "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m" + "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m" + } + Write-Host "$($colors[$Color])$Message$($colors['Reset'])" + } else { + Write-Host $Message + } +} + +# Data structures +class ControllerInfo { + [string]$ConstructorName # NewAccountController + [string]$Domain # AccountController + [string]$VarName # accountController + [System.Collections.Generic.List[Parameter]]$Parameters + + ControllerInfo() { + $this.Parameters = [System.Collections.Generic.List[Parameter]]::new() + } +} + +class Parameter { + [string]$Name + [string]$RawType + [string]$NormalizedType +} + +function Get-LowerCamelCase { + param([string]$Text) + if ($Text.Length -eq 0) { return $Text } + return $Text.Substring(0, 1).ToLower() + $Text.Substring(1) +} + +function Normalize-TypeName { + param([string]$TypeStr) + + # Remove leading pointer + $cleaned = $TypeStr -replace '^\*+', '' + + # Remove package prefix (everything before last dot) + if ($cleaned -match '\.([^.]+)$') { + $cleaned = $matches[1] + } + + return $cleaned.Trim() +} + +function Parse-GoFiles { + param([string]$Directory) + + Write-ColorOutput "Scanning for controller constructors in $Directory..." "Cyan" + + if (-not (Test-Path $Directory)) { + Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red" + exit 1 + } + + $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File + $controllers = [System.Collections.Generic.List[ControllerInfo]]::new() + + foreach ($file in $goFiles) { + $content = Get-Content $file.FullName -Raw + + # Match function signatures (support multi-line) + # Pattern: func NewXxxController(...) XxxController + $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Controller)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Controller)' + $matches = [regex]::Matches($content, $pattern) + + foreach ($match in $matches) { + $constructorName = $match.Groups[1].Value + $paramsStr = $match.Groups[2].Value + $returnType = $match.Groups[3].Value + + # Extract domain name (XxxController) + $domain = Normalize-TypeName $returnType + $varName = Get-LowerCamelCase $domain + + $controller = [ControllerInfo]::new() + $controller.ConstructorName = $constructorName + $controller.Domain = $domain + $controller.VarName = $varName + + # Parse parameters + if ($paramsStr.Trim() -ne "") { + # Split by comma, but be careful with nested types + $paramList = $paramsStr -split ',\s*(?![^<>]*>)' + + foreach ($param in $paramList) { + $param = $param.Trim() + if ($param -eq "") { continue } + + # Split into name and type + $parts = $param -split '\s+', 2 + + $p = [Parameter]::new() + if ($parts.Count -eq 2) { + $p.Name = $parts[0] + $p.RawType = $parts[1] + } elseif ($parts.Count -eq 1) { + # Anonymous parameter - synthesize name + $p.Name = "param$($controller.Parameters.Count)" + $p.RawType = $parts[0] + } else { + continue + } + + $p.NormalizedType = Normalize-TypeName $p.RawType + $controller.Parameters.Add($p) + } + } + + $controllers.Add($controller) + Write-ColorOutput " Found: $constructorName" "Green" + } + } + + if ($controllers.Count -eq 0) { + Write-ColorOutput "No controller constructors found matching pattern 'NewXxxController'!" "Red" + exit 1 + } + + Write-ColorOutput "`nTotal controllers discovered: $($controllers.Count)" "Blue" + return $controllers +} + +function Resolve-ControllerArgument { + param([Parameter]$Param) + + $type = $Param.NormalizedType + + # DEPENDENCY RESOLUTION RULES + # ============================================ + + # 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService() + if ($type -match '^(.+)Service$') { + $serviceName = $type + return "servicesProvider.Provide${serviceName}()" + } + + # ADD MORE SPECIAL CASES HERE: + # -------------------------------------------- + # Example: Config dependency + # if ($type -eq "Config") { + # return "configProvider.ProvideConfig()" + # } + # + # Example: Logger + # if ($type -eq "Logger") { + # return "loggerProvider.ProvideLogger()" + # } + # + # Example: Validator + # if ($type -eq "Validator") { + # return "validatorProvider.ProvideValidator()" + # } + # -------------------------------------------- + + # 2. Fallback: unresolved type + return "/* TODO: provide $($Param.RawType) */" +} + +function Generate-ProviderCode { + param([System.Collections.Generic.List[ControllerInfo]]$Controllers) + + Write-ColorOutput "`nGenerating controller provider code..." "Cyan" + + # Sort controllers alphabetically for consistent output + $sortedControllers = $Controllers | Sort-Object -Property Domain + + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("package provider") + [void]$sb.AppendLine() + [void]$sb.AppendLine("import `"$ModulePath`"") + [void]$sb.AppendLine() + + # Interface + [void]$sb.AppendLine("type ControllerProvider interface {") + foreach ($ctrl in $sortedControllers) { + $line = "`tProvide$($ctrl.Domain)() controllers.$($ctrl.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Struct + [void]$sb.AppendLine("type controllerProvider struct {") + foreach ($ctrl in $sortedControllers) { + $line = "`t$($ctrl.VarName) controllers.$($ctrl.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Constructor + [void]$sb.AppendLine("func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider {") + [void]$sb.AppendLine() + + # Initialize controllers + foreach ($ctrl in $sortedControllers) { + $args = @() + foreach ($param in $ctrl.Parameters) { + $args += Resolve-ControllerArgument $param + } + $argsStr = $args -join ", " + $line = "`t$($ctrl.VarName) := controllers.$($ctrl.ConstructorName)($argsStr)" + [void]$sb.AppendLine($line) + } + + [void]$sb.AppendLine("`treturn &controllerProvider{") + foreach ($ctrl in $sortedControllers) { + $line = "`t`t$($ctrl.VarName): $($ctrl.VarName)," + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("`t}") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Getter methods + [void]$sb.AppendLine("// --- Getter Methods ---") + [void]$sb.AppendLine() + foreach ($ctrl in $sortedControllers) { + [void]$sb.AppendLine("func (c *controllerProvider) Provide$($ctrl.Domain)() controllers.$($ctrl.Domain) {") + [void]$sb.AppendLine("`treturn c.$($ctrl.VarName)") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + } + + return $sb.ToString() +} + +function Write-ProviderFile { + param([string]$Code, [string]$OutputPath) + + Write-ColorOutput "Writing to $OutputPath..." "Cyan" + + # Ensure directory exists + $dir = Split-Path $OutputPath -Parent + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + + # Write file as UTF-8 without BOM + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom) + + Write-ColorOutput " Successfully generated $OutputPath" "Green" +} + +# ============================================ +# MAIN EXECUTION +# ============================================ + +try { + Write-ColorOutput "`n=========================================" "Blue" + Write-ColorOutput " Go Controller Provider Generator v1.0" "Blue" + Write-ColorOutput "=========================================`n" "Blue" + + # Step 1: Parse all controller constructors + $controllers = Parse-GoFiles -Directory $ControllersDir + + # Step 2: Generate provider code + $code = Generate-ProviderCode -Controllers $controllers + + # Step 3: Write to file + Write-ProviderFile -Code $code -OutputPath $OutputFile + + Write-ColorOutput "`nSUCCESS! Controller provider generated successfully.`n" "Green" + Write-ColorOutput "Next steps:" "Cyan" + Write-ColorOutput " 1. Review $OutputFile" "White" + Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White" + Write-ColorOutput " 3. Run: go build ./provider" "White" + +} catch { + Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red" + Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow" + exit 1 +} \ No newline at end of file diff --git a/cmd/do_inject_middleware.ps1 b/cmd/do_inject_middleware.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a2de970e0565598b5e3ea29f6dd252e700ec195c --- /dev/null +++ b/cmd/do_inject_middleware.ps1 @@ -0,0 +1,312 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Automatic Dependency Injection Generator for Go Middleware + +.DESCRIPTION + Scans ./middleware/ directory, discovers all middleware constructors, infers their dependencies, + and generates provider/middleware_provider.go with full DI wiring. + +.EXAMPLE + .\middleware_injector.ps1 + +.NOTES + - Works with PowerShell 5.1+ and PowerShell 7+ + - No external dependencies required + - Supports multi-line constructor signatures + - Middleware depend on services from ServicesProvider +#> + +[CmdletBinding()] +param() + +# Configuration +$MiddlewareDir = "./middleware" +$OutputFile = "provider/middleware_provider.go" +$ModulePath = "abdanhafidz.com/go-boilerplate/middleware" + +# ANSI colors for better output +$script:UseColors = $Host.UI.SupportsVirtualTerminal +function Write-ColorOutput { + param([string]$Message, [string]$Color = "White") + if ($script:UseColors) { + $colors = @{ + "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m" + "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m" + } + Write-Host "$($colors[$Color])$Message$($colors['Reset'])" + } else { + Write-Host $Message + } +} + +# Data structures +class MiddlewareInfo { + [string]$ConstructorName # NewAuthenticationMiddleware + [string]$Domain # AuthenticationMiddleware + [string]$VarName # authenticationMiddleware + [System.Collections.Generic.List[Parameter]]$Parameters + + MiddlewareInfo() { + $this.Parameters = [System.Collections.Generic.List[Parameter]]::new() + } +} + +class Parameter { + [string]$Name + [string]$RawType + [string]$NormalizedType +} + +function Get-LowerCamelCase { + param([string]$Text) + if ($Text.Length -eq 0) { return $Text } + return $Text.Substring(0, 1).ToLower() + $Text.Substring(1) +} + +function Normalize-TypeName { + param([string]$TypeStr) + + # Remove leading pointer + $cleaned = $TypeStr -replace '^\*+', '' + + # Remove package prefix (everything before last dot) + if ($cleaned -match '\.([^.]+)$') { + $cleaned = $matches[1] + } + + return $cleaned.Trim() +} + +function Parse-GoFiles { + param([string]$Directory) + + Write-ColorOutput "Scanning for middleware constructors in $Directory..." "Cyan" + + if (-not (Test-Path $Directory)) { + Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red" + exit 1 + } + + $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File + $middlewares = [System.Collections.Generic.List[MiddlewareInfo]]::new() + + foreach ($file in $goFiles) { + $content = Get-Content $file.FullName -Raw + + # Match function signatures (support multi-line) + # Pattern: func NewXxxMiddleware(...) XxxMiddleware + $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Middleware)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Middleware)' + $matches = [regex]::Matches($content, $pattern) + + foreach ($match in $matches) { + $constructorName = $match.Groups[1].Value + $paramsStr = $match.Groups[2].Value + $returnType = $match.Groups[3].Value + + # Extract domain name (XxxMiddleware) + $domain = Normalize-TypeName $returnType + $varName = Get-LowerCamelCase $domain + + $middleware = [MiddlewareInfo]::new() + $middleware.ConstructorName = $constructorName + $middleware.Domain = $domain + $middleware.VarName = $varName + + # Parse parameters + if ($paramsStr.Trim() -ne "") { + # Split by comma, but be careful with nested types + $paramList = $paramsStr -split ',\s*(?![^<>]*>)' + + foreach ($param in $paramList) { + $param = $param.Trim() + if ($param -eq "") { continue } + + # Split into name and type + $parts = $param -split '\s+', 2 + + $p = [Parameter]::new() + if ($parts.Count -eq 2) { + $p.Name = $parts[0] + $p.RawType = $parts[1] + } elseif ($parts.Count -eq 1) { + # Anonymous parameter - synthesize name + $p.Name = "param$($middleware.Parameters.Count)" + $p.RawType = $parts[0] + } else { + continue + } + + $p.NormalizedType = Normalize-TypeName $p.RawType + $middleware.Parameters.Add($p) + } + } + + $middlewares.Add($middleware) + Write-ColorOutput " Found: $constructorName" "Green" + } + } + + if ($middlewares.Count -eq 0) { + Write-ColorOutput "No middleware constructors found matching pattern 'NewXxxMiddleware'!" "Red" + exit 1 + } + + Write-ColorOutput "`nTotal middleware discovered: $($middlewares.Count)" "Blue" + return $middlewares +} + +function Resolve-MiddlewareArgument { + param([Parameter]$Param) + + $type = $Param.NormalizedType + + # DEPENDENCY RESOLUTION RULES + # ============================================ + + # 1. Service pattern: XxxxService -> servicesProvider.ProvideXxxxService() + if ($type -match '^(.+)Service$') { + $serviceName = $type + return "servicesProvider.Provide${serviceName}()" + } + + # ADD MORE SPECIAL CASES HERE: + # -------------------------------------------- + # Example: Config dependency + # if ($type -eq "Config") { + # return "configProvider.ProvideConfig()" + # } + # + # Example: Logger + # if ($type -eq "Logger") { + # return "loggerProvider.ProvideLogger()" + # } + # + # Example: JWT Config + # if ($type -eq "JWTConfig") { + # return "configProvider.ProvideJWTConfig()" + # } + # + # Example: Database + # if ($type -eq "DB" -or $type -eq "Database") { + # return "dbProvider.ProvideDatabase()" + # } + # -------------------------------------------- + + # 2. Fallback: unresolved type + return "/* TODO: provide $($Param.RawType) */" +} + +function Generate-ProviderCode { + param([System.Collections.Generic.List[MiddlewareInfo]]$Middlewares) + + Write-ColorOutput "`nGenerating middleware provider code..." "Cyan" + + # Sort middleware alphabetically for consistent output + $sortedMiddlewares = $Middlewares | Sort-Object -Property Domain + + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("package provider") + [void]$sb.AppendLine() + [void]$sb.AppendLine("import `"$ModulePath`"") + [void]$sb.AppendLine() + + # Interface + [void]$sb.AppendLine("type MiddlewareProvider interface {") + foreach ($mw in $sortedMiddlewares) { + $line = "`tProvide$($mw.Domain)() middleware.$($mw.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Struct + [void]$sb.AppendLine("type middlewareProvider struct {") + foreach ($mw in $sortedMiddlewares) { + $line = "`t$($mw.VarName) middleware.$($mw.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Constructor + [void]$sb.AppendLine("func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider {") + + # Initialize middleware + foreach ($mw in $sortedMiddlewares) { + $args = @() + foreach ($param in $mw.Parameters) { + $args += Resolve-MiddlewareArgument $param + } + $argsStr = $args -join ", " + $line = "`t$($mw.VarName) := middleware.$($mw.ConstructorName)($argsStr)" + [void]$sb.AppendLine($line) + } + + [void]$sb.AppendLine("`treturn &middlewareProvider{") + foreach ($mw in $sortedMiddlewares) { + $line = "`t`t$($mw.VarName): $($mw.VarName)," + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("`t}") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Getter methods + foreach ($mw in $sortedMiddlewares) { + [void]$sb.AppendLine("func (p *middlewareProvider) Provide$($mw.Domain)() middleware.$($mw.Domain) {") + [void]$sb.AppendLine("`treturn p.$($mw.VarName)") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + } + + return $sb.ToString() +} + +function Write-ProviderFile { + param([string]$Code, [string]$OutputPath) + + Write-ColorOutput "Writing to $OutputPath..." "Cyan" + + # Ensure directory exists + $dir = Split-Path $OutputPath -Parent + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + + # Write file as UTF-8 without BOM + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom) + + Write-ColorOutput " Successfully generated $OutputPath" "Green" +} + +# ============================================ +# MAIN EXECUTION +# ============================================ + +try { + Write-ColorOutput "`n=========================================" "Blue" + Write-ColorOutput " Go Middleware Provider Generator v1.0" "Blue" + Write-ColorOutput "=========================================`n" "Blue" + + # Step 1: Parse all middleware constructors + $middlewares = Parse-GoFiles -Directory $MiddlewareDir + + # Step 2: Generate provider code + $code = Generate-ProviderCode -Middlewares $middlewares + + # Step 3: Write to file + Write-ProviderFile -Code $code -OutputPath $OutputFile + + Write-ColorOutput "`nSUCCESS! Middleware provider generated successfully.`n" "Green" + Write-ColorOutput "Next steps:" "Cyan" + Write-ColorOutput " 1. Review $OutputFile" "White" + Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White" + Write-ColorOutput " 3. Run: go build ./provider" "White" + +} catch { + Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red" + Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow" + exit 1 +} \ No newline at end of file diff --git a/cmd/do_inject_repository.ps1 b/cmd/do_inject_repository.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..2d2bb3baec54ccb34441d6274307f44c8e654c18 --- /dev/null +++ b/cmd/do_inject_repository.ps1 @@ -0,0 +1,327 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Automatic Dependency Injection Generator for Go Repositories + +.DESCRIPTION + Scans ./repositories/ directory, discovers all repository constructors, + and generates provider/repositories_provider.go with full DI wiring. + Repositories typically depend on database connection from ConfigProvider. + +.EXAMPLE + .\repository_injector.ps1 + +.NOTES + - Works with PowerShell 5.1+ and PowerShell 7+ + - No external dependencies required + - Supports multi-line constructor signatures + - Repositories depend on database instance from ConfigProvider +#> + +[CmdletBinding()] +param() + +# Configuration +$RepositoriesDir = "./repositories" +$OutputFile = "provider/repositories_provider.go" +$ModulePath = "abdanhafidz.com/go-boilerplate/repositories" + +# ANSI colors for better output +$script:UseColors = $Host.UI.SupportsVirtualTerminal +function Write-ColorOutput { + param([string]$Message, [string]$Color = "White") + if ($script:UseColors) { + $colors = @{ + "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m" + "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m" + } + Write-Host "$($colors[$Color])$Message$($colors['Reset'])" + } else { + Write-Host $Message + } +} + +# Data structures +class RepositoryInfo { + [string]$ConstructorName # NewAccountRepository + [string]$Domain # AccountRepository + [string]$VarName # accountRepository + [System.Collections.Generic.List[Parameter]]$Parameters + + RepositoryInfo() { + $this.Parameters = [System.Collections.Generic.List[Parameter]]::new() + } +} + +class Parameter { + [string]$Name + [string]$RawType + [string]$NormalizedType +} + +function Get-LowerCamelCase { + param([string]$Text) + if ($Text.Length -eq 0) { return $Text } + return $Text.Substring(0, 1).ToLower() + $Text.Substring(1) +} + +function Normalize-TypeName { + param([string]$TypeStr) + + # Remove leading pointer + $cleaned = $TypeStr -replace '^\*+', '' + + # Remove package prefix (everything before last dot) + if ($cleaned -match '\.([^.]+)$') { + $cleaned = $matches[1] + } + + return $cleaned.Trim() +} + +function Parse-GoFiles { + param([string]$Directory) + + Write-ColorOutput "Scanning for repository constructors in $Directory..." "Cyan" + + if (-not (Test-Path $Directory)) { + Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red" + exit 1 + } + + $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File + $repositories = [System.Collections.Generic.List[RepositoryInfo]]::new() + + foreach ($file in $goFiles) { + $content = Get-Content $file.FullName -Raw + + # Match function signatures (support multi-line) + # Pattern: func NewXxxRepository(...) XxxRepository + $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Repository)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Repository)' + $matches = [regex]::Matches($content, $pattern) + + foreach ($match in $matches) { + $constructorName = $match.Groups[1].Value + $paramsStr = $match.Groups[2].Value + $returnType = $match.Groups[3].Value + + # Extract domain name (XxxRepository) + $domain = Normalize-TypeName $returnType + $varName = Get-LowerCamelCase $domain + + $repo = [RepositoryInfo]::new() + $repo.ConstructorName = $constructorName + $repo.Domain = $domain + $repo.VarName = $varName + + # Parse parameters + if ($paramsStr.Trim() -ne "") { + # Split by comma, but be careful with nested types + $paramList = $paramsStr -split ',\s*(?![^<>]*>)' + + foreach ($param in $paramList) { + $param = $param.Trim() + if ($param -eq "") { continue } + + # Split into name and type + $parts = $param -split '\s+', 2 + + $p = [Parameter]::new() + if ($parts.Count -eq 2) { + $p.Name = $parts[0] + $p.RawType = $parts[1] + } elseif ($parts.Count -eq 1) { + # Anonymous parameter - synthesize name + $p.Name = "param$($repo.Parameters.Count)" + $p.RawType = $parts[0] + } else { + continue + } + + $p.NormalizedType = Normalize-TypeName $p.RawType + $repo.Parameters.Add($p) + } + } + + $repositories.Add($repo) + Write-ColorOutput " Found: $constructorName" "Green" + } + } + + if ($repositories.Count -eq 0) { + Write-ColorOutput "No repository constructors found matching pattern 'NewXxxRepository'!" "Red" + exit 1 + } + + Write-ColorOutput "`nTotal repositories discovered: $($repositories.Count)" "Blue" + return $repositories +} + +function Resolve-RepositoryArgument { + param([Parameter]$Param) + + $type = $Param.NormalizedType + $paramName = $Param.Name + + # DEPENDENCY RESOLUTION RULES FOR REPOSITORIES + # ============================================ + + # 1. Database connection patterns + if ($type -match '^(DB|Database|Gorm|SqlDB|Connection)$' -or $paramName -match '^(db|database|conn|connection)$') { + return "db" + } + + # 2. *gorm.DB (most common in Go GORM projects) + if ($Param.RawType -match 'gorm\.DB' -or $type -eq "DB") { + return "db" + } + + # 3. *sql.DB (standard library) + if ($Param.RawType -match 'sql\.DB') { + return "db" + } + + # ADD MORE SPECIAL CASES HERE: + # -------------------------------------------- + # Example: Redis connection + # if ($type -eq "RedisClient" -or $paramName -match "redis") { + # return "redisClient" + # } + # + # Example: MongoDB connection + # if ($type -eq "MongoClient" -or $paramName -match "mongo") { + # return "mongoClient" + # } + # + # Example: Cache + # if ($type -eq "Cache" -or $paramName -match "cache") { + # return "cache" + # } + # + # Example: Logger + # if ($type -eq "Logger" -or $paramName -match "logger") { + # return "logger" + # } + # -------------------------------------------- + + # 4. Fallback: unresolved type + return "/* TODO: provide $($Param.RawType) */" +} + +function Generate-ProviderCode { + param([System.Collections.Generic.List[RepositoryInfo]]$Repositories) + + Write-ColorOutput "`nGenerating repositories provider code..." "Cyan" + + # Sort repositories alphabetically for consistent output + $sortedRepos = $Repositories | Sort-Object -Property Domain + + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("package provider") + [void]$sb.AppendLine() + [void]$sb.AppendLine("import `"$ModulePath`"") + [void]$sb.AppendLine() + + # Interface + [void]$sb.AppendLine("type RepositoriesProvider interface {") + foreach ($repo in $sortedRepos) { + $line = "`tProvide$($repo.Domain)() repositories.$($repo.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Struct + [void]$sb.AppendLine("type repositoriesProvider struct {") + foreach ($repo in $sortedRepos) { + $line = "`t$($repo.VarName) repositories.$($repo.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Constructor + [void]$sb.AppendLine("func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider {") + [void]$sb.AppendLine("`tdbConfig := cfg.ProvideDatabaseConfig()") + [void]$sb.AppendLine("`tdb := dbConfig.GetInstance()") + [void]$sb.AppendLine() + + # Initialize repositories + foreach ($repo in $sortedRepos) { + $args = @() + foreach ($param in $repo.Parameters) { + $args += Resolve-RepositoryArgument $param + } + $argsStr = $args -join ", " + $line = "`t$($repo.VarName) := repositories.$($repo.ConstructorName)($argsStr)" + [void]$sb.AppendLine($line) + } + + [void]$sb.AppendLine() + [void]$sb.AppendLine("`treturn &repositoriesProvider{") + foreach ($repo in $sortedRepos) { + $line = "`t`t$($repo.VarName): $($repo.VarName)," + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("`t}") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Getter methods + foreach ($repo in $sortedRepos) { + [void]$sb.AppendLine("func (r *repositoriesProvider) Provide$($repo.Domain)() repositories.$($repo.Domain) {") + [void]$sb.AppendLine("`treturn r.$($repo.VarName)") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + } + + return $sb.ToString() +} + +function Write-ProviderFile { + param([string]$Code, [string]$OutputPath) + + Write-ColorOutput "Writing to $OutputPath..." "Cyan" + + # Ensure directory exists + $dir = Split-Path $OutputPath -Parent + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + + # Write file as UTF-8 without BOM + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom) + + Write-ColorOutput " Successfully generated $OutputPath" "Green" +} + +# ============================================ +# MAIN EXECUTION +# ============================================ + +try { + Write-ColorOutput "`n=========================================" "Blue" + Write-ColorOutput " Go Repository Provider Generator v1.0" "Blue" + Write-ColorOutput "=========================================`n" "Blue" + + # Step 1: Parse all repository constructors + $repositories = Parse-GoFiles -Directory $RepositoriesDir + + # Step 2: Generate provider code + $code = Generate-ProviderCode -Repositories $repositories + + # Step 3: Write to file + Write-ProviderFile -Code $code -OutputPath $OutputFile + + Write-ColorOutput "`nSUCCESS! Repositories provider generated successfully.`n" "Green" + Write-ColorOutput "Next steps:" "Cyan" + Write-ColorOutput " 1. Review $OutputFile" "White" + Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White" + Write-ColorOutput " 3. Run: go build ./provider" "White" + +} catch { + Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red" + Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow" + exit 1 +} \ No newline at end of file diff --git a/cmd/do_inject_services.ps1 b/cmd/do_inject_services.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..27a3d4e599a52ee1b0577ca5d6c2359606261b47 --- /dev/null +++ b/cmd/do_inject_services.ps1 @@ -0,0 +1,383 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Automatic Dependency Injection Generator for Go Services + +.DESCRIPTION + Scans ./services/ directory, discovers service constructors, resolves dependencies, + performs topological sorting, and generates provider/services_provider.go with full DI wiring. + +.EXAMPLE + .\service_injector.ps1 + +.NOTES + - Works with PowerShell 5.1+ and PowerShell 7+ + - No external dependencies required + - Supports multi-line constructor signatures + - Handles dependency cycles with clear error messages +#> + +[CmdletBinding()] +param() + +# Configuration +$ServicesDir = "./services" +$OutputFile = "provider/services_provider.go" +$ModulePath = "abdanhafidz.com/go-boilerplate/services" + +# ANSI colors for better output (fallback to plain text if not supported) +$script:UseColors = $Host.UI.SupportsVirtualTerminal +function Write-ColorOutput { + param([string]$Message, [string]$Color = "White") + if ($script:UseColors) { + $colors = @{ + "Green" = "`e[32m"; "Yellow" = "`e[33m"; "Red" = "`e[31m" + "Cyan" = "`e[36m"; "Blue" = "`e[34m"; "Reset" = "`e[0m" + } + Write-Host "$($colors[$Color])$Message$($colors['Reset'])" + } else { + Write-Host $Message + } +} + +# Data structures +class ServiceInfo { + [string]$ConstructorName # NewAccountService + [string]$Domain # AccountService + [string]$VarName # accountService + [System.Collections.Generic.List[Parameter]]$Parameters + [System.Collections.Generic.List[string]]$ServiceDependencies + + ServiceInfo() { + $this.Parameters = [System.Collections.Generic.List[Parameter]]::new() + $this.ServiceDependencies = [System.Collections.Generic.List[string]]::new() + } +} + +class Parameter { + [string]$Name + [string]$RawType + [string]$NormalizedType +} + +function Get-LowerCamelCase { + param([string]$Text) + if ($Text.Length -eq 0) { return $Text } + return $Text.Substring(0, 1).ToLower() + $Text.Substring(1) +} + +function Normalize-TypeName { + param([string]$TypeStr) + + # Remove leading pointer + $cleaned = $TypeStr -replace '^\*+', '' + + # Remove package prefix (everything before last dot) + if ($cleaned -match '\.([^.]+)$') { + $cleaned = $matches[1] + } + + return $cleaned.Trim() +} + +function Parse-GoFiles { + param([string]$Directory) + + Write-ColorOutput "Scanning for service constructors in $Directory..." "Cyan" + + if (-not (Test-Path $Directory)) { + Write-ColorOutput "ERROR: Directory '$Directory' not found!" "Red" + exit 1 + } + + $goFiles = Get-ChildItem -Path $Directory -Filter "*.go" -Recurse -File + $services = [System.Collections.Generic.List[ServiceInfo]]::new() + + foreach ($file in $goFiles) { + $content = Get-Content $file.FullName -Raw + + # Match function signatures (support multi-line) + # Pattern: func NewXxxService(...) XxxService + $pattern = '(?ms)func\s+(New[a-zA-Z0-9]+Service)\s*\(([^)]*)\)\s+([a-zA-Z0-9*_.]+Service)' + $matches = [regex]::Matches($content, $pattern) + + foreach ($match in $matches) { + $constructorName = $match.Groups[1].Value + $paramsStr = $match.Groups[2].Value + $returnType = $match.Groups[3].Value + + # Extract domain name (XxxService) + $domain = Normalize-TypeName $returnType + $varName = Get-LowerCamelCase $domain + + $service = [ServiceInfo]::new() + $service.ConstructorName = $constructorName + $service.Domain = $domain + $service.VarName = $varName + + # Parse parameters + if ($paramsStr.Trim() -ne "") { + # Split by comma, but be careful with nested types + $paramList = $paramsStr -split ',\s*(?![^<>]*>)' + + foreach ($param in $paramList) { + $param = $param.Trim() + if ($param -eq "") { continue } + + # Split into name and type + $parts = $param -split '\s+', 2 + + $p = [Parameter]::new() + if ($parts.Count -eq 2) { + $p.Name = $parts[0] + $p.RawType = $parts[1] + } elseif ($parts.Count -eq 1) { + # Anonymous parameter - synthesize name + $p.Name = "param$($service.Parameters.Count)" + $p.RawType = $parts[0] + } else { + continue + } + + $p.NormalizedType = Normalize-TypeName $p.RawType + $service.Parameters.Add($p) + + # Track service dependencies + if ($p.NormalizedType -match 'Service$') { + $service.ServiceDependencies.Add($p.NormalizedType) + } + } + } + + $services.Add($service) + Write-ColorOutput " Found: $constructorName" "Green" + } + } + + if ($services.Count -eq 0) { + Write-ColorOutput "No service constructors found matching pattern 'NewXxxService'!" "Red" + exit 1 + } + + Write-ColorOutput "`nTotal services discovered: $($services.Count)" "Blue" + return $services +} + +function Get-TopologicalOrder { + param([System.Collections.Generic.List[ServiceInfo]]$Services) + + Write-ColorOutput "`nBuilding dependency graph..." "Cyan" + + # Build adjacency list + $graph = @{} + $inDegree = @{} + $domainToService = @{} + + foreach ($svc in $Services) { + $graph[$svc.Domain] = [System.Collections.Generic.List[string]]::new() + $inDegree[$svc.Domain] = 0 + $domainToService[$svc.Domain] = $svc + } + + # Build edges + foreach ($svc in $Services) { + foreach ($dep in $svc.ServiceDependencies) { + if ($graph.ContainsKey($dep)) { + $graph[$dep].Add($svc.Domain) + $inDegree[$svc.Domain]++ + } + } + } + + # Kahn's algorithm for topological sort + $queue = [System.Collections.Generic.Queue[string]]::new() + foreach ($domain in $inDegree.Keys) { + if ($inDegree[$domain] -eq 0) { + $queue.Enqueue($domain) + } + } + + $sorted = [System.Collections.Generic.List[string]]::new() + + while ($queue.Count -gt 0) { + $current = $queue.Dequeue() + $sorted.Add($current) + + foreach ($neighbor in $graph[$current]) { + $inDegree[$neighbor]-- + if ($inDegree[$neighbor] -eq 0) { + $queue.Enqueue($neighbor) + } + } + } + + # Check for cycles + if ($sorted.Count -ne $Services.Count) { + $remaining = $inDegree.Keys | Where-Object { $inDegree[$_] -gt 0 } + Write-ColorOutput "`nERROR: Circular dependency detected!" "Red" + Write-ColorOutput "Services involved in cycle: $($remaining -join ', ')" "Yellow" + exit 1 + } + + Write-ColorOutput " Dependency graph validated (no cycles)" "Green" + Write-ColorOutput " Topological order: $($sorted -join ' -> ')" "Blue" + + # Return services in topological order + return $sorted | ForEach-Object { $domainToService[$_] } +} + +function Resolve-ConstructorArgument { + param([Parameter]$Param) + + $type = $Param.NormalizedType + + # SPECIAL CASE MAPPINGS - Add more here as needed + # ============================================ + + # 1. JWT secret string + if ($Param.RawType -eq "string" -and $Param.Name -match "secret|key") { + return "configProvider.ProvideJWTConfig().GetSecretKey()" + } + + # 2. Repository pattern: XxxxRepository -> repoProvider.ProvideXxxxRepository() + if ($type -match '^(.+)Repository$') { + $repoName = $matches[1] + return "repoProvider.Provide${repoName}Repository()" + } + + # 3. Service pattern: XxxxService -> use variable (will be constructed before this) + if ($type -match 'Service$') { + return (Get-LowerCamelCase $type) + } + + # ADD MORE SPECIAL CASES HERE: + # -------------------------------------------- + # Example: Mail config + # if ($type -eq "MailConfig") { + # return "configProvider.ProvideMailConfig()" + # } + # + # Example: Redis client + # if ($type -eq "RedisClient") { + # return "configProvider.ProvideRedisClient()" + # } + # -------------------------------------------- + + # 4. Fallback: unresolved type + return "/* TODO: provide $($Param.RawType) */" +} + +function Generate-ProviderCode { + param([System.Collections.Generic.List[ServiceInfo]]$ServicesInOrder) + + Write-ColorOutput "`nGenerating provider code..." "Cyan" + + $sb = [System.Text.StringBuilder]::new() + [void]$sb.AppendLine("package provider") + [void]$sb.AppendLine() + [void]$sb.AppendLine("import `"$ModulePath`"") + [void]$sb.AppendLine() + + # Interface + [void]$sb.AppendLine("type ServicesProvider interface {") + foreach ($svc in $ServicesInOrder) { + $line = "`tProvide$($svc.Domain)() services.$($svc.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Struct + [void]$sb.AppendLine("type servicesProvider struct {") + foreach ($svc in $ServicesInOrder) { + $line = "`t$($svc.VarName) services.$($svc.Domain)" + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Constructor + [void]$sb.AppendLine("func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider {") + + # Initialize services in topological order + foreach ($svc in $ServicesInOrder) { + $args = @() + foreach ($param in $svc.Parameters) { + $args += Resolve-ConstructorArgument $param + } + $argsStr = $args -join ", " + $line = "`t$($svc.VarName) := services.$($svc.ConstructorName)($argsStr)" + [void]$sb.AppendLine($line) + } + + [void]$sb.AppendLine() + [void]$sb.AppendLine("`treturn &servicesProvider{") + foreach ($svc in $ServicesInOrder) { + $line = "`t`t$($svc.VarName): $($svc.VarName)," + [void]$sb.AppendLine($line) + } + [void]$sb.AppendLine("`t}") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + + # Getter methods + foreach ($svc in $ServicesInOrder) { + [void]$sb.AppendLine("func (s *servicesProvider) Provide$($svc.Domain)() services.$($svc.Domain) {") + [void]$sb.AppendLine("`treturn s.$($svc.VarName)") + [void]$sb.AppendLine("}") + [void]$sb.AppendLine() + } + + return $sb.ToString() +} + +function Write-ProviderFile { + param([string]$Code, [string]$OutputPath) + + Write-ColorOutput "Writing to $OutputPath..." "Cyan" + + # Ensure directory exists + $dir = Split-Path $OutputPath -Parent + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + + # Write file as UTF-8 without BOM + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($OutputPath, $Code, $utf8NoBom) + + Write-ColorOutput " Successfully generated $OutputPath" "Green" +} + +# ============================================ +# MAIN EXECUTION +# ============================================ + +try { + Write-ColorOutput "`n=========================================" "Blue" + Write-ColorOutput " Go Service Dependency Injector v1.0" "Blue" + Write-ColorOutput "=========================================`n" "Blue" + + # Step 1: Parse all service constructors + $services = Parse-GoFiles -Directory $ServicesDir + + # Step 2: Perform topological sort + $sortedServices = Get-TopologicalOrder -Services $services + + # Step 3: Generate provider code + $code = Generate-ProviderCode -ServicesInOrder $sortedServices + + # Step 4: Write to file + Write-ProviderFile -Code $code -OutputPath $OutputFile + + Write-ColorOutput "`nSUCCESS! Provider generated successfully.`n" "Green" + Write-ColorOutput "Next steps:" "Cyan" + Write-ColorOutput " 1. Review $OutputFile" "White" + Write-ColorOutput " 2. Fill any /* TODO: provide ... */ placeholders" "White" + Write-ColorOutput " 3. Run: go build ./provider" "White" + +} catch { + Write-ColorOutput "`nERROR: $($_.Exception.Message)" "Red" + Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Yellow" + exit 1 +} \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000000000000000000000000000000000000..d912156bec00a9f00850ab2ec3a3baf1016c2141 --- /dev/null +++ b/config/config.go @@ -0,0 +1 @@ +package config diff --git a/config/database_config.go b/config/database_config.go new file mode 100644 index 0000000000000000000000000000000000000000..49fa139c5a57256db20791f7a41f9f1895bea853 --- /dev/null +++ b/config/database_config.go @@ -0,0 +1,55 @@ +package config + +import ( + "fmt" + "log" + + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type DatabaseConfig interface { + AutoMigrateAll(entities ...interface{}) error + GetInstance() *gorm.DB +} +type databaseConfig struct { + db *gorm.DB +} + +func NewDatabaseConfig(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT string) DatabaseConfig { + dsn := fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Jakarta ", + DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT, + ) + + db, err := gorm.Open(postgres.New(postgres.Config{ + DSN: dsn, + PreferSimpleProtocol: true, // required for PgBouncer/Supabase pooled connections + }), &gorm.Config{ + TranslateError: true, + }) + + db = db.Session(&gorm.Session{ + PrepareStmt: false, + }) + + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + + return &databaseConfig{db: db} +} + +func (cfg *databaseConfig) AutoMigrateAll(entities ...interface{}) error { + + err := cfg.db.AutoMigrate( + entities..., + ) + + return err + +} + +func (cfg *databaseConfig) GetInstance() *gorm.DB { + return cfg.db +} diff --git a/config/env_config.go b/config/env_config.go new file mode 100644 index 0000000000000000000000000000000000000000..ffce96bfb38847a2efed3902e6c6cea850f43d67 --- /dev/null +++ b/config/env_config.go @@ -0,0 +1,112 @@ +package config + +import ( + "os" + "strconv" + "strings" + "abdanhafidz.com/go-boilerplate/utils" + "github.com/joho/godotenv" +) + +type EnvConfig interface { + GetTCPAddress() string + GetLogPath() string + GetHostAddress() string + GetHostPort() string + GetEmailVerificationDuration() int + GetDatabaseHost() string + GetDatabasePort() string + GetDatabaseUser() string + GetDatabasePassword() string + GetDatabaseName() string + GetSalt() string + GetSupabaseURL() string + GetSupabaseKey() string + GetSupabaseBucket() string + GetXenditAPIKey() string + GetXenditCallbackToken() string +} + +type envConfig struct { + timezone string +} + +func NewEnvConfig(timezone string) EnvConfig { + godotenv.Load() + os.Setenv("TZ", timezone) + return &envConfig{ + timezone: timezone, + } +} + +func (e *envConfig) GetTCPAddress() string { + return utils.GetEnv("HOST_ADDRESS") + ":" + utils.GetEnv("HOST_PORT") +} + +func (e *envConfig) GetLogPath() string { + return utils.GetEnv("LOG_PATH") +} + +func (e *envConfig) GetHostAddress() string { + return utils.GetEnv("HOST_ADDRESS") +} + +func (e *envConfig) GetHostPort() string { + return utils.GetEnv("HOST_PORT") +} + +func (e *envConfig) GetEmailVerificationDuration() int { + duration, err := strconv.Atoi(utils.GetEnv("EMAIL_VERIFICATION_DURATION")) + if err != nil { + return 0 // Default value if parsing fails + } + return duration +} + +func (e *envConfig) GetDatabaseHost() string { + return utils.GetEnv("DB_HOST") +} + +func (e *envConfig) GetDatabasePort() string { + return utils.GetEnv("DB_PORT") +} + +func (e *envConfig) GetDatabaseUser() string { + return utils.GetEnv("DB_USER") +} + +func (e *envConfig) GetDatabasePassword() string { + return utils.GetEnv("DB_PASSWORD") +} + +func (e *envConfig) GetDatabaseName() string { + return utils.GetEnv("DB_NAME") +} + +func (e *envConfig) GetSalt() string { + salt := utils.GetEnv("SALT") + if salt == "" { + return "Def4u|7" // Default salt value + } + return salt +} + +func (e *envConfig) GetSupabaseURL() string { + return strings.TrimSpace(utils.GetEnv("SUPABASE_URL")) +} + +func (e *envConfig) GetSupabaseKey() string { + return strings.TrimSpace(utils.GetEnv("SUPABASE_SERVICE_KEY")) +} + +func (e *envConfig) GetSupabaseBucket() string { + return strings.TrimSpace(utils.GetEnv("SUPABASE_BUCKET_NAME")) +} + +func (e *envConfig) GetXenditAPIKey() string { + return strings.TrimSpace(utils.GetEnv("XENDIT_API_KEY")) +} + +func (e *envConfig) GetXenditCallbackToken() string { + return strings.TrimSpace(utils.GetEnv("XENDIT_CALLBACK_TOKEN")) +} diff --git a/config/jwt_config.go b/config/jwt_config.go new file mode 100644 index 0000000000000000000000000000000000000000..d9555ffcba8cc622a18ff4a32b90aff3d3402194 --- /dev/null +++ b/config/jwt_config.go @@ -0,0 +1,24 @@ +package config + +type JWTConfig interface { + SetSecretKey(key string) + GetSecretKey() string +} + +type jwtConfig struct { + secretKey string +} + +func NewJWTConfig(secretKey string) JWTConfig { + return &jwtConfig{ + secretKey: secretKey, + } +} + +func (cfg *jwtConfig) SetSecretKey(key string) { + cfg.secretKey = key +} + +func (cfg *jwtConfig) GetSecretKey() string { + return cfg.secretKey +} diff --git a/config/supabase_config.go b/config/supabase_config.go new file mode 100644 index 0000000000000000000000000000000000000000..47b07527e06bbf911d446ebd09a69d95ee61edbf --- /dev/null +++ b/config/supabase_config.go @@ -0,0 +1,25 @@ +package config + +type SupabaseConfig interface { + GetURL() string + GetServiceKey() string + GetBucketName() string +} + +type supabaseConfig struct { + url string + serviceKey string + bucketName string +} + +func NewSupabaseConfig(url string, key string, bucket string) SupabaseConfig { + return &supabaseConfig{ + url: url, + serviceKey: key, + bucketName: bucket, + } +} + +func (c *supabaseConfig) GetURL() string { return c.url } +func (c *supabaseConfig) GetServiceKey() string { return c.serviceKey } +func (c *supabaseConfig) GetBucketName() string { return c.bucketName } diff --git a/config/upload_config.go b/config/upload_config.go new file mode 100644 index 0000000000000000000000000000000000000000..0792731ae1cd436aa7380f91637e56eaf8da7663 --- /dev/null +++ b/config/upload_config.go @@ -0,0 +1,46 @@ +package config + +import ( + models "abdanhafidz.com/go-boilerplate/models/entity" + http_error "abdanhafidz.com/go-boilerplate/models/error" +) + +type UploadRule struct { + MaxBytes int64 + AllowedExts map[string]bool + PathPrefix string + MaxCount int +} + +type UploadConfig interface { + Get(contextType string) (UploadRule, error) +} + +type uploadConfig struct{} + +func NewUploadConfig() UploadConfig { return &uploadConfig{} } + +func (c *uploadConfig) Get(contextType string) (UploadRule, error) { + codeExts := map[string]bool{".cpp": true, ".c": true, ".py": true, ".java": true, ".go": true, ".js": true, ".txt": true} + imgExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".webp": true, ".gif": true} + docExts := map[string]bool{".pdf": true, ".doc": true, ".docx": true} + + allExts := make(map[string]bool) + for k, v := range codeExts { allExts[k] = v } + for k, v := range imgExts { allExts[k] = v } + for k, v := range docExts { allExts[k] = v } + + switch contextType { + case "image": + return UploadRule{ MaxBytes: 10 * models.MB, AllowedExts: imgExts, PathPrefix: "images", MaxCount: 5 }, nil + case "material": + return UploadRule{ MaxBytes: 10 * models.MB, AllowedExts: docExts, PathPrefix: "materials", MaxCount: 1 }, nil + case "submission": + return UploadRule{ MaxBytes: 1 * models.MB, AllowedExts: codeExts, PathPrefix: "submissions", MaxCount: 1 }, nil + case "general": + return UploadRule{ MaxBytes: 5 * models.MB, AllowedExts: allExts, PathPrefix: "temp", MaxCount: 5 }, nil + default: + return UploadRule{}, http_error.INVALID_UPLOAD_CONTEXT_ERROR + } +} + diff --git a/config/xendit_config.go b/config/xendit_config.go new file mode 100644 index 0000000000000000000000000000000000000000..f08d81375b1df1de6dac441160fae9e7a16bc0b4 --- /dev/null +++ b/config/xendit_config.go @@ -0,0 +1,31 @@ +package config + +import ( + "sync" + + xendit "github.com/xendit/xendit-go/v7" +) + +var ( + xenditOnce sync.Once +) + +type XenditConfig interface { + GetClient() *xendit.APIClient +} + +type xenditConfig struct { + envConfig EnvConfig + client *xendit.APIClient +} + +func NewXenditConfig(envConfig EnvConfig) XenditConfig { + return &xenditConfig{ + envConfig: envConfig, + client: xendit.NewClient(envConfig.GetXenditAPIKey()), + } +} + +func (c *xenditConfig) GetClient() *xendit.APIClient { + return c.client +} diff --git a/controllers/authentication_controller.go b/controllers/authentication_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..24a9de2880eebe2195e8ca0f967a2a0dc2ca73b6 --- /dev/null +++ b/controllers/authentication_controller.go @@ -0,0 +1,59 @@ +package controllers + +import ( + "abdanhafidz.com/go-boilerplate/models/dto" + entity "abdanhafidz.com/go-boilerplate/models/entity" + "abdanhafidz.com/go-boilerplate/services" + "github.com/gin-gonic/gin" +) + +type AuthenticationController interface { + SignUp(ctx *gin.Context) + SignIn(ctx *gin.Context) + ChangePassword(ctx *gin.Context) + UpdateUserRole(ctx *gin.Context) +} + +type authenticationController struct { + userService services.UserService +} + +func NewAuthenticationController(userService services.UserService) AuthenticationController { + return &authenticationController{ + userService: userService, + } +} + +func (c *authenticationController) SignUp(ctx *gin.Context) { + req := RequestJSON[dto.SignUpRequest](ctx) + res, err := c.userService.Create(ctx.Request.Context(), req.Name, req.Email, req.Username, req.Password) + ResponseJSON(ctx, req, res, err) +} + +func (c *authenticationController) SignIn(ctx *gin.Context) { + req := RequestJSON[dto.SignInRequest](ctx) + res, err := c.userService.Validate(ctx, req.EmailorUsername, req.Password) + ResponseJSON(ctx, req, res, err) +} + +func (c *authenticationController) ChangePassword(ctx *gin.Context) { + req := RequestJSON[dto.ChangePasswordRequest](ctx) + userId := ParseUserId(ctx) + res, err := c.userService.ChangePassword(ctx.Request.Context(), userId, req.OldPassword, req.NewPassword) + ResponseJSON(ctx, req, res, err) +} + +func (c *authenticationController) UpdateUserRole(ctx *gin.Context) { + req := RequestJSON[dto.UpdateUserRoleRequest](ctx) + userId := ctx.Param("userId") + user, err := c.userService.GetById(ctx.Request.Context(), userId) + if err != nil { + ResponseJSON(ctx, req, entity.User{}, err) + return + } + + // For now, don't implement full Role update logic (requires roles association in schema) + // Just stub it + res, err := c.userService.Update(ctx.Request.Context(), user) + ResponseJSON(ctx, req, res, err) +} diff --git a/controllers/controller.go b/controllers/controller.go new file mode 100644 index 0000000000000000000000000000000000000000..420328a4ad32ffc43ffe58cf227f4615be738c48 --- /dev/null +++ b/controllers/controller.go @@ -0,0 +1,53 @@ +package controllers + +import ( + http_error "abdanhafidz.com/go-boilerplate/models/error" + "abdanhafidz.com/go-boilerplate/utils" + "github.com/gin-gonic/gin" + uuid "github.com/google/uuid" +) + +func ParseUserId(ctx *gin.Context) string { + guserId, _ := ctx.Get("user_id") + userId, ok := guserId.(string) + if !ok { + ResponseJSON(ctx, gin.H{"user_id": userId}, "", http_error.INVALID_TOKEN) + return "" + } + return userId +} + +func ParseUUID(ctx *gin.Context, attrName string) uuid.UUID { + uuidRaw, _ := ctx.Get(attrName) + uuidParsed, err := utils.ToUUID(uuidRaw) + + if err != nil { + ResponseJSON(ctx, gin.H{"id": uuidParsed}, uuid.UUID{}, http_error.INVALID_TOKEN) + return uuid.UUID{} + } + return uuidParsed +} +func RequestJSON[TRequest any](ctx *gin.Context) TRequest { + var request TRequest + if err := ctx.ShouldBindJSON(&request); err != nil { + utils.ResponseFAILED(ctx, request, http_error.BAD_REQUEST_ERROR) + ctx.Abort() + return request + } else { + return request + } +} + +func RequestForm[TRequest any](ctx *gin.Context) TRequest { + var request TRequest + if err := ctx.ShouldBind(&request); err != nil { + utils.ResponseFAILED(ctx, request, http_error.BAD_REQUEST_ERROR) + ctx.Abort() + return request + } + return request +} + +func ResponseJSON[TResponse any, TMetaData any](ctx *gin.Context, metaData TMetaData, res TResponse, err error) { + utils.SendResponse(ctx, metaData, res, err) +} diff --git a/controllers/home_controller.go b/controllers/home_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..3664c73b7bbb3f4aa11662f3e9ad6edf9573ec46 --- /dev/null +++ b/controllers/home_controller.go @@ -0,0 +1,113 @@ +package controllers + +import ( + "abdanhafidz.com/go-boilerplate/services" + "github.com/gin-gonic/gin" +) + +type HomeController interface { + GetHomeData(ctx *gin.Context) + GetSchedules(ctx *gin.Context) + GetEventCategories(ctx *gin.Context) + GetClassCategories(ctx *gin.Context) +} + +type homeController struct { + homeService services.HomeService +} + +func NewHomeController(homeService services.HomeService) HomeController { + return &homeController{homeService: homeService} +} + +// GetHomeData godoc +// @Summary Get Home Page Data +// @Description Return all sections needed by the home page in one aggregated response. +// @Tags Home Dashboard +// @Accept json +// @Produce json +// @Param date query string false "Selected date for schedule card section (YYYY-MM-DD)" +// @Param latitude query number false "User latitude" +// @Param longitude query number false "User longitude" +// @Param city query string false "User city" +// @Success 200 {object} dto.HomeResponseWrapper +// @Failure 400 {object} dto.ErrorResponse +// @Security BearerAuth +// @Router /api/v1/home [get] +func (c *homeController) GetHomeData(ctx *gin.Context) { + userId := ParseUserId(ctx) + + params := map[string]interface{}{ + "date": ctx.Query("date"), + "latitude": ctx.Query("latitude"), + "longitude": ctx.Query("longitude"), + "city": ctx.Query("city"), + } + + res, err := c.homeService.GetHomeData(ctx.Request.Context(), userId, params) + ResponseJSON(ctx, "", res, err) +} + +// GetSchedules godoc +// @Summary Get Schedule List +// @Description Get a list of class or event schedules based on date range, city, filters, etc. +// @Tags Home Dashboard +// @Accept json +// @Produce json +// @Param date query string false "Exact date (YYYY-MM-DD)" +// @Param start_date query string false "Start date range (YYYY-MM-DD)" +// @Param end_date query string false "End date range (YYYY-MM-DD)" +// @Param city query string false "Filter by city" +// @Param level query string false "Filter by class level (e.g., beginner)" +// @Param category query string false "Filter by class category (e.g., salsa)" +// @Param page query int false "Page number (default: 1)" +// @Param limit query int false "Items per page (default: 10)" +// @Success 200 {object} dto.ScheduleListResponseWrapper +// @Failure 400 {object} dto.ErrorResponse +// @Security BearerAuth +// @Router /api/v1/schedules [get] +func (c *homeController) GetSchedules(ctx *gin.Context) { + userId := ParseUserId(ctx) + + params := map[string]interface{}{ + "date": ctx.Query("date"), + "start_date": ctx.Query("start_date"), + "end_date": ctx.Query("end_date"), + "city": ctx.Query("city"), + "level": ctx.Query("level"), + "category": ctx.Query("category"), + "page": ctx.Query("page"), + "limit": ctx.Query("limit"), + } + + res, err := c.homeService.GetSchedules(ctx.Request.Context(), userId, params) + ResponseJSON(ctx, "", res, err) +} + +// GetEventCategories godoc +// @Summary Get Event Categories +// @Description Get master list of event categories +// @Tags Master Data +// @Accept json +// @Produce json +// @Success 200 {object} dto.CategoryResponseWrapper +// @Failure 400 {object} dto.ErrorResponse +// @Router /api/v1/event-categories [get] +func (c *homeController) GetEventCategories(ctx *gin.Context) { + res, err := c.homeService.GetEventCategories(ctx.Request.Context()) + ResponseJSON(ctx, "", res, err) +} + +// GetClassCategories godoc +// @Summary Get Class Categories +// @Description Get master list of class categories +// @Tags Master Data +// @Accept json +// @Produce json +// @Success 200 {object} dto.CategoryResponseWrapper +// @Failure 400 {object} dto.ErrorResponse +// @Router /api/v1/class-categories [get] +func (c *homeController) GetClassCategories(ctx *gin.Context) { + res, err := c.homeService.GetClassCategories(ctx.Request.Context()) + ResponseJSON(ctx, "", res, err) +} diff --git a/controllers/otp_verification_controller.go b/controllers/otp_verification_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..35bdace55483ac8ebe43ed03542615c8a46ec1b8 --- /dev/null +++ b/controllers/otp_verification_controller.go @@ -0,0 +1,42 @@ +package controllers + +import ( + "abdanhafidz.com/go-boilerplate/services" + "github.com/gin-gonic/gin" +) + +type OTPVerificationController interface { + RequestOTP(ctx *gin.Context) + VerifyOTP(ctx *gin.Context) +} + +type otpVerificationController struct { + otpService services.OTPVerificationService +} + +func NewOTPVerificationController(otpService services.OTPVerificationService) OTPVerificationController { + return &otpVerificationController{ + otpService: otpService, + } +} + +type RequestOTPPayload struct { + PhoneNumber string `json:"phone_number" binding:"required"` +} + +type VerifyOTPPayload struct { + PhoneNumber string `json:"phone_number" binding:"required"` + Code string `json:"code" binding:"required"` +} + +func (c *otpVerificationController) RequestOTP(ctx *gin.Context) { + req := RequestJSON[RequestOTPPayload](ctx) + err := c.otpService.RequestOTP(ctx.Request.Context(), req.PhoneNumber) + ResponseJSON(ctx, req, "", err) +} + +func (c *otpVerificationController) VerifyOTP(ctx *gin.Context) { + req := RequestJSON[VerifyOTPPayload](ctx) + err := c.otpService.VerifyOTP(ctx.Request.Context(), req.PhoneNumber, req.Code) + ResponseJSON(ctx, req, "", err) +} diff --git a/controllers/payment_callback_controller.go b/controllers/payment_callback_controller.go new file mode 100644 index 0000000000000000000000000000000000000000..2e3706f879ff6090549dd16abc1afd346a96c845 --- /dev/null +++ b/controllers/payment_callback_controller.go @@ -0,0 +1,79 @@ +package controllers + +import ( + "log" + + "abdanhafidz.com/go-boilerplate/models/dto" + http_error "abdanhafidz.com/go-boilerplate/models/error" + "abdanhafidz.com/go-boilerplate/services" + "abdanhafidz.com/go-boilerplate/utils" + "github.com/gin-gonic/gin" +) + +type PaymentCallbackController interface { + HandleCallback(ctx *gin.Context) +} + +type paymentCallbackController struct { + paymentService services.PaymentService +} + +func NewPaymentCallbackController( + paymentService services.PaymentService, +) PaymentCallbackController { + return &paymentCallbackController{ + paymentService: paymentService, + } +} + +// Handle Payment Callback godoc +// @Summary Handle Xendit Payment Callback +// @Description Receive and process payment status updates from Xendit +// @Tags Payment +// @Accept json +// @Produce json +// @Param request body map[string]interface{} true "Xendit Callback Payload" +// @Success 200 {object} dto.SuccessResponse[any] +// @Failure 400 {object} dto.ErrorResponse +// @Router /api/v1/payment/callback [post] +func (c *paymentCallbackController) HandleCallback(ctx *gin.Context) { + // Xendit sends JSON payload + // Basic structure for Invoice Callback: + // { "id": "...", "external_id": "...", "status": "PAID", ... } + var callbackData map[string]interface{} + if err := ctx.ShouldBindJSON(&callbackData); err != nil { + utils.ResponseFAILED(ctx, gin.H(nil), http_error.BAD_REQUEST_ERROR) + return + } + + log.Printf("Payment Callback Received: %+v", callbackData) + + status, ok := callbackData["status"].(string) + if !ok { + // Not a status update or unknown format + var _ dto.SuccessResponse[any] + + ResponseJSON(ctx, gin.H(nil), "Ignored: No status", nil) + return + } + + invoiceId, _ := callbackData["id"].(string) + + if status == "PAID" || status == "SETTLED" { + // Handle Event Payment + // We need a method in Service to handle "ConfirmPayment" by InvoiceID + // But existing services don't have it. + // Let's add it to PaymentService? Yes. + err := c.paymentService.ConfirmPayment(ctx.Request.Context(), invoiceId) + if err != nil { + log.Printf("Payment Confirmation Failed: %v", err) + // Don't return error to Xendit if logic failed, but maybe we should? + // Xendit expects 200 OK. + } + } else if status == "EXPIRED" { + c.paymentService.ExpirePayment(ctx.Request.Context(), invoiceId) + } + + // Always return 200 to Xendit + ResponseJSON(ctx, gin.H(nil), gin.H{"callback": "Callback Received"}, nil) +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000000000000000000000000000000000000..b12d160623429930b19ed052a84a8927f3da1db3 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,705 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/class-categories": { + "get": { + "description": "Get master list of class categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Class Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/event-categories": { + "get": { + "description": "Get master list of event categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Event Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/home": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Return all sections needed by the home page in one aggregated response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Home Page Data", + "parameters": [ + { + "type": "string", + "description": "Selected date for schedule card section (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "number", + "description": "User latitude", + "name": "latitude", + "in": "query" + }, + { + "type": "number", + "description": "User longitude", + "name": "longitude", + "in": "query" + }, + { + "type": "string", + "description": "User city", + "name": "city", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.HomeResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/payment/callback": { + "post": { + "description": "Receive and process payment status updates from Xendit", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Payment" + ], + "summary": "Handle Xendit Payment Callback", + "parameters": [ + { + "description": "Xendit Callback Payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuccessResponse-any" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/schedules": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a list of class or event schedules based on date range, city, filters, etc.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Schedule List", + "parameters": [ + { + "type": "string", + "description": "Exact date (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "string", + "description": "Start date range (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date range (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Filter by city", + "name": "city", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class level (e.g., beginner)", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class category (e.g., salsa)", + "name": "category", + "in": "query" + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Items per page (default: 10)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ScheduleListResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "dto.CategoryResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CategoryResponseWrapper": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CategoryResponse" + } + }, + "success": { + "type": "boolean" + } + } + }, + "dto.ErrorResponse": { + "type": "object", + "properties": { + "errors": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + }, + "dto.HomeBeginnerClassSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeDay": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "day_name_short": { + "type": "string" + }, + "day_number": { + "type": "integer" + }, + "has_schedule": { + "type": "boolean" + }, + "is_selected": { + "type": "boolean" + } + } + }, + "dto.HomeEventSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeHeroBanner": { + "type": "object", + "properties": { + "badge_text": { + "type": "string" + }, + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.HomeNotifications": { + "type": "object", + "properties": { + "unread_count": { + "type": "integer" + } + } + }, + "dto.HomePromoStrip": { + "type": "object", + "properties": { + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "dto.HomeResponse": { + "type": "object", + "properties": { + "beginner_class_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeBeginnerClassSection" + } + }, + "event_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeEventSection" + } + }, + "hero_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeHeroBanner" + } + }, + "notifications": { + "$ref": "#/definitions/dto.HomeNotifications" + }, + "promo_strip": { + "$ref": "#/definitions/dto.HomePromoStrip" + }, + "search": { + "$ref": "#/definitions/dto.HomeSearch" + }, + "user": { + "$ref": "#/definitions/dto.HomeUser" + }, + "week_schedule": { + "$ref": "#/definitions/dto.HomeWeekSchedule" + } + } + }, + "dto.HomeResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.HomeResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.HomeSearch": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + } + } + }, + "dto.HomeUser": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, + "dto.HomeWeekSchedule": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeDay" + } + }, + "end_date": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleItem" + } + }, + "selected_date": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "view_all_url": { + "type": "string" + }, + "week_label": { + "type": "string" + } + } + }, + "dto.PaginationMeta": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "total_items": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "dto.PriceData": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "display": { + "type": "string" + } + } + }, + "dto.ScheduleItem": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListItem": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "available_slots": { + "type": "integer" + }, + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "level": { + "type": "string" + }, + "price": { + "$ref": "#/definitions/dto.PriceData" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "thumbnail_url": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleListItem" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationMeta" + } + } + }, + "dto.ScheduleListResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ScheduleListResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.SuccessResponse-any": { + "type": "object", + "properties": { + "data": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000000000000000000000000000000000000..466b4e94235d108a6b8b02c387c1bfebfab5c4af --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,676 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api/v1/class-categories": { + "get": { + "description": "Get master list of class categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Class Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/event-categories": { + "get": { + "description": "Get master list of event categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Event Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/home": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Return all sections needed by the home page in one aggregated response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Home Page Data", + "parameters": [ + { + "type": "string", + "description": "Selected date for schedule card section (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "number", + "description": "User latitude", + "name": "latitude", + "in": "query" + }, + { + "type": "number", + "description": "User longitude", + "name": "longitude", + "in": "query" + }, + { + "type": "string", + "description": "User city", + "name": "city", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.HomeResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/payment/callback": { + "post": { + "description": "Receive and process payment status updates from Xendit", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Payment" + ], + "summary": "Handle Xendit Payment Callback", + "parameters": [ + { + "description": "Xendit Callback Payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuccessResponse-any" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/schedules": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a list of class or event schedules based on date range, city, filters, etc.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Schedule List", + "parameters": [ + { + "type": "string", + "description": "Exact date (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "string", + "description": "Start date range (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date range (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Filter by city", + "name": "city", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class level (e.g., beginner)", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class category (e.g., salsa)", + "name": "category", + "in": "query" + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Items per page (default: 10)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ScheduleListResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "dto.CategoryResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CategoryResponseWrapper": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CategoryResponse" + } + }, + "success": { + "type": "boolean" + } + } + }, + "dto.ErrorResponse": { + "type": "object", + "properties": { + "errors": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + }, + "dto.HomeBeginnerClassSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeDay": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "day_name_short": { + "type": "string" + }, + "day_number": { + "type": "integer" + }, + "has_schedule": { + "type": "boolean" + }, + "is_selected": { + "type": "boolean" + } + } + }, + "dto.HomeEventSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeHeroBanner": { + "type": "object", + "properties": { + "badge_text": { + "type": "string" + }, + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.HomeNotifications": { + "type": "object", + "properties": { + "unread_count": { + "type": "integer" + } + } + }, + "dto.HomePromoStrip": { + "type": "object", + "properties": { + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "dto.HomeResponse": { + "type": "object", + "properties": { + "beginner_class_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeBeginnerClassSection" + } + }, + "event_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeEventSection" + } + }, + "hero_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeHeroBanner" + } + }, + "notifications": { + "$ref": "#/definitions/dto.HomeNotifications" + }, + "promo_strip": { + "$ref": "#/definitions/dto.HomePromoStrip" + }, + "search": { + "$ref": "#/definitions/dto.HomeSearch" + }, + "user": { + "$ref": "#/definitions/dto.HomeUser" + }, + "week_schedule": { + "$ref": "#/definitions/dto.HomeWeekSchedule" + } + } + }, + "dto.HomeResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.HomeResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.HomeSearch": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + } + } + }, + "dto.HomeUser": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, + "dto.HomeWeekSchedule": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeDay" + } + }, + "end_date": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleItem" + } + }, + "selected_date": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "view_all_url": { + "type": "string" + }, + "week_label": { + "type": "string" + } + } + }, + "dto.PaginationMeta": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "total_items": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "dto.PriceData": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "display": { + "type": "string" + } + } + }, + "dto.ScheduleItem": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListItem": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "available_slots": { + "type": "integer" + }, + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "level": { + "type": "string" + }, + "price": { + "$ref": "#/definitions/dto.PriceData" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "thumbnail_url": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleListItem" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationMeta" + } + } + }, + "dto.ScheduleListResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ScheduleListResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.SuccessResponse-any": { + "type": "object", + "properties": { + "data": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ef312819e90d7cbd7505adcdb367d02951e25a93 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,440 @@ +definitions: + dto.CategoryResponse: + properties: + id: + type: string + image_url: + type: string + name: + type: string + type: object + dto.CategoryResponseWrapper: + properties: + data: + items: + $ref: '#/definitions/dto.CategoryResponse' + type: array + success: + type: boolean + type: object + dto.ErrorResponse: + properties: + errors: {} + message: {} + meta_data: {} + status: + type: string + type: object + dto.HomeBeginnerClassSection: + properties: + id: + type: string + image_url: + type: string + name: + type: string + type: + type: string + type: object + dto.HomeDay: + properties: + date: + type: string + day_name_short: + type: string + day_number: + type: integer + has_schedule: + type: boolean + is_selected: + type: boolean + type: object + dto.HomeEventSection: + properties: + id: + type: string + image_url: + type: string + name: + type: string + type: + type: string + type: object + dto.HomeHeroBanner: + properties: + badge_text: + type: string + cta_target_id: + type: string + cta_text: + type: string + cta_type: + type: string + id: + type: string + image_url: + type: string + subtitle: + type: string + title: + type: string + type: object + dto.HomeNotifications: + properties: + unread_count: + type: integer + type: object + dto.HomePromoStrip: + properties: + cta_target_id: + type: string + cta_text: + type: string + cta_type: + type: string + id: + type: string + text: + type: string + type: object + dto.HomeResponse: + properties: + beginner_class_sections: + items: + $ref: '#/definitions/dto.HomeBeginnerClassSection' + type: array + event_sections: + items: + $ref: '#/definitions/dto.HomeEventSection' + type: array + hero_banners: + items: + $ref: '#/definitions/dto.HomeHeroBanner' + type: array + notifications: + $ref: '#/definitions/dto.HomeNotifications' + promo_strip: + $ref: '#/definitions/dto.HomePromoStrip' + search: + $ref: '#/definitions/dto.HomeSearch' + user: + $ref: '#/definitions/dto.HomeUser' + week_schedule: + $ref: '#/definitions/dto.HomeWeekSchedule' + type: object + dto.HomeResponseWrapper: + properties: + data: + $ref: '#/definitions/dto.HomeResponse' + message: + type: string + success: + type: boolean + type: object + dto.HomeSearch: + properties: + placeholder: + type: string + type: object + dto.HomeUser: + properties: + avatar_url: + type: string + first_name: + type: string + full_name: + type: string + id: + type: string + type: object + dto.HomeWeekSchedule: + properties: + days: + items: + $ref: '#/definitions/dto.HomeDay' + type: array + end_date: + type: string + items: + items: + $ref: '#/definitions/dto.ScheduleItem' + type: array + selected_date: + type: string + start_date: + type: string + view_all_url: + type: string + week_label: + type: string + type: object + dto.PaginationMeta: + properties: + limit: + type: integer + page: + type: integer + total_items: + type: integer + total_pages: + type: integer + type: object + dto.PriceData: + properties: + amount: + type: integer + currency: + type: string + display: + type: string + type: object + dto.ScheduleItem: + properties: + city: + type: string + class_id: + type: string + date: + type: string + display_time: + type: string + end_time: + type: string + is_booked: + type: boolean + schedule_id: + type: string + start_time: + type: string + studio_name: + type: string + teacher_name: + type: string + title: + type: string + type: object + dto.ScheduleListItem: + properties: + address: + type: string + available_slots: + type: integer + category: + type: string + city: + type: string + class_id: + type: string + date: + type: string + display_time: + type: string + end_time: + type: string + is_booked: + type: boolean + level: + type: string + price: + $ref: '#/definitions/dto.PriceData' + schedule_id: + type: string + start_time: + type: string + studio_name: + type: string + teacher_name: + type: string + thumbnail_url: + type: string + title: + type: string + type: object + dto.ScheduleListResponse: + properties: + items: + items: + $ref: '#/definitions/dto.ScheduleListItem' + type: array + pagination: + $ref: '#/definitions/dto.PaginationMeta' + type: object + dto.ScheduleListResponseWrapper: + properties: + data: + $ref: '#/definitions/dto.ScheduleListResponse' + message: + type: string + success: + type: boolean + type: object + dto.SuccessResponse-any: + properties: + data: {} + message: {} + meta_data: {} + status: + type: string + type: object +info: + contact: {} +paths: + /api/v1/class-categories: + get: + consumes: + - application/json + description: Get master list of class categories + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CategoryResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Get Class Categories + tags: + - Master Data + /api/v1/event-categories: + get: + consumes: + - application/json + description: Get master list of event categories + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CategoryResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Get Event Categories + tags: + - Master Data + /api/v1/home: + get: + consumes: + - application/json + description: Return all sections needed by the home page in one aggregated response. + parameters: + - description: Selected date for schedule card section (YYYY-MM-DD) + in: query + name: date + type: string + - description: User latitude + in: query + name: latitude + type: number + - description: User longitude + in: query + name: longitude + type: number + - description: User city + in: query + name: city + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.HomeResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + security: + - BearerAuth: [] + summary: Get Home Page Data + tags: + - Home Dashboard + /api/v1/payment/callback: + post: + consumes: + - application/json + description: Receive and process payment status updates from Xendit + parameters: + - description: Xendit Callback Payload + in: body + name: request + required: true + schema: + additionalProperties: true + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SuccessResponse-any' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Handle Xendit Payment Callback + tags: + - Payment + /api/v1/schedules: + get: + consumes: + - application/json + description: Get a list of class or event schedules based on date range, city, + filters, etc. + parameters: + - description: Exact date (YYYY-MM-DD) + in: query + name: date + type: string + - description: Start date range (YYYY-MM-DD) + in: query + name: start_date + type: string + - description: End date range (YYYY-MM-DD) + in: query + name: end_date + type: string + - description: Filter by city + in: query + name: city + type: string + - description: Filter by class level (e.g., beginner) + in: query + name: level + type: string + - description: Filter by class category (e.g., salsa) + in: query + name: category + type: string + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + - description: 'Items per page (default: 10)' + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ScheduleListResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + security: + - BearerAuth: [] + summary: Get Schedule List + tags: + - Home Dashboard +swagger: "2.0" diff --git a/err.txt b/err.txt new file mode 100644 index 0000000000000000000000000000000000000000..c19304eca9dabb4c8513130e984c4b49f224ff05 --- /dev/null +++ b/err.txt @@ -0,0 +1,17 @@ +# abdanhafidz.com/go-boilerplate/repositories +repositories\account_details_repository.go:12:58: undefined: entity.AccountDetail +repositories\account_details_repository.go:13:66: undefined: entity.AccountDetail +repositories\account_details_repository.go:14:80: undefined: entity.AccountDetail +repositories\account_details_repository.go:15:53: undefined: entity.AccountDetail +repositories\account_details_repository.go:16:58: undefined: entity.AccountDetail +repositories\account_repository.go:12:52: undefined: entity.Account +repositories\account_repository.go:13:67: undefined: entity.Account +repositories\account_repository.go:14:63: undefined: entity.Account +repositories\account_repository.go:15:69: undefined: entity.Account +repositories\account_repository.go:16:47: undefined: entity.Account +repositories\account_repository.go:16:47: too many errors +# abdanhafidz.com/go-boilerplate/models/dto +models\dto\account_details_dto.go:8:17: undefined: entity.Account +models\dto\account_details_dto.go:9:17: undefined: entity.AccountDetail +models\dto\authentication_dto.go:53:17: undefined: entity.Account +models\dto\option_dto.go:11:19: undefined: entity.Options diff --git a/err2.txt b/err2.txt new file mode 100644 index 0000000000000000000000000000000000000000..cdee0074c3a85f58f8053a4974c9c84573ba347f --- /dev/null +++ b/err2.txt @@ -0,0 +1,12 @@ +# abdanhafidz.com/go-boilerplate/repositories +repositories\account_repository.go:12:52: undefined: entity.Account +repositories\account_repository.go:13:67: undefined: entity.Account +repositories\account_repository.go:14:63: undefined: entity.Account +repositories\account_repository.go:15:69: undefined: entity.Account +repositories\account_repository.go:16:47: undefined: entity.Account +repositories\account_repository.go:17:52: undefined: entity.Account +repositories\account_repository.go:30:79: undefined: entity.Account +repositories\email_verification_repository.go:13:50: undefined: entity.EmailVerification +repositories\email_verification_repository.go:14:85: undefined: entity.EmailVerification +repositories\email_verification_repository.go:17:73: undefined: entity.EmailVerification +repositories\account_repository.go:30:79: too many errors diff --git a/err3.txt b/err3.txt new file mode 100644 index 0000000000000000000000000000000000000000..e316443f5ab6d28190578a430897feabbe9ac700 --- /dev/null +++ b/err3.txt @@ -0,0 +1,7 @@ +# abdanhafidz.com/go-boilerplate/services +services\user_service.go:70:11: assignment mismatch: 1 variable but s.jwtService.GenerateToken returns 2 values +services\user_service.go:70:38: cannot use user.ID (variable of type string) as context.Context value in argument to s.jwtService.GenerateToken: string does not implement context.Context (missing method Deadline) +services\user_service.go:70:47: cannot use "" (untyped string constant) as dto.JWTCustomClaims value in argument to s.jwtService.GenerateToken +services\user_service.go:100:11: assignment mismatch: 1 variable but s.jwtService.GenerateToken returns 2 values +services\user_service.go:100:38: cannot use user.ID (variable of type string) as context.Context value in argument to s.jwtService.GenerateToken: string does not implement context.Context (missing method Deadline) +services\user_service.go:100:47: cannot use "" (untyped string constant) as dto.JWTCustomClaims value in argument to s.jwtService.GenerateToken diff --git a/err4.txt b/err4.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/err_swag.txt b/err_swag.txt new file mode 100644 index 0000000000000000000000000000000000000000..e50acf25f7c325305bc79dd6dd9106bdbf8faac9 Binary files /dev/null and b/err_swag.txt differ diff --git a/err_swag2.txt b/err_swag2.txt new file mode 100644 index 0000000000000000000000000000000000000000..3966a64ab8753700404648c2eab2906ba6d05e74 --- /dev/null +++ b/err_swag2.txt @@ -0,0 +1,3 @@ +2026/03/10 20:32:28 Generate swagger docs.... +2026/03/10 20:32:28 Generate general API Info, search dir:./ +2026/03/10 20:32:30 ParseComment error in file C:\Users\asus\projects\danzapp-be\controllers\home_controller.go for comment: '// @Success 200 {object} dto.SuccessResponse[dto.HomeResponse]': cannot find type definition: dto.SuccessResponse[dto.HomeResponse] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..03f5f3e3e44ba2f0863bfa160d8112afac57d905 --- /dev/null +++ b/go.mod @@ -0,0 +1,75 @@ +module abdanhafidz.com/go-boilerplate + +go 1.25.4 + +require ( + github.com/gin-contrib/gzip v1.2.5 + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 + github.com/xendit/xendit-go/v7 v7.0.0 + golang.org/x/crypto v0.46.0 + gorm.io/datatypes v1.2.7 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/spec v0.22.2 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.5 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.58.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gorm.io/driver/mysql v1.5.6 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..00ca2635b1ce759357bf27fb33af5781ff9e9308 --- /dev/null +++ b/go.sum @@ -0,0 +1,215 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= +github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc= +github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.0 h1:5YBPNs273uzsZJD1I8uiB4Aqg9sN6sMDVX3s6LxmhWU= +github.com/go-playground/validator/v10 v10.30.0/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= +github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= +github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xendit/xendit-go/v7 v7.0.0 h1:A7Nhaulk1a+mOI/KgRcvb5VSQEB6nhsUGkAhi+RkrEM= +github.com/xendit/xendit-go/v7 v7.0.0/go.mod h1:W562aw0zhjzF/OUhZLc77q2iFQc9INa5tBy5xl6OLbo= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/go.work b/go.work new file mode 100644 index 0000000000000000000000000000000000000000..64f7a0918490948650cb721072be2ce796fbeff1 --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.25.4 + +use . diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000000000000000000000000000000000000..6ea781f3030d6cb1696de809f0a2580ccb00c76e --- /dev/null +++ b/go.work.sum @@ -0,0 +1,91 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= +cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= +cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= +cloud.google.com/go/translate v1.10.3 h1:g+B29z4gtRGsiKDoTF+bNeH25bLRokAaElygX2FcZkE= +cloud.google.com/go/translate v1.10.3/go.mod h1:GW0vC1qvPtd3pgtypCv4k4U8B7EdgK9/QEF2aJEUovs= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= +github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20251014184007-4626949a642f h1:T/BJL1nqPyWStq45hQyss5sEketltFJ/eWERyJ98U5M= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20251014184007-4626949a642f/go.mod h1:ejCb7yLmK6GCVHp5qpeKbm4KZew/ldg+9b8kq5MONgk= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/logs/error_log.txt b/logs/error_log.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/logs/security_log.txt b/logs/security_log.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/main.go b/main.go new file mode 100644 index 0000000000000000000000000000000000000000..38bebbf35f6847694118ba0dece049649a83639e --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "abdanhafidz.com/go-boilerplate/provider" + "abdanhafidz.com/go-boilerplate/router" +) + +func main() { + appProvider := provider.NewAppProvider() + router.RunRouter(appProvider) +} diff --git a/middleware/authentication_middleware.go b/middleware/authentication_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..2586e05e224672b10a8ec8626ab56f2e7e0d4e0b --- /dev/null +++ b/middleware/authentication_middleware.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "errors" + "fmt" + "strings" + + http_error "abdanhafidz.com/go-boilerplate/models/error" + "abdanhafidz.com/go-boilerplate/services" + utils "abdanhafidz.com/go-boilerplate/utils" + "github.com/gin-gonic/gin" +) + +type AuthenticationMiddleware interface { + VerifyAccount(ctx *gin.Context) +} +type authenticationMiddleware struct { + jwtService services.JWTService +} + +func NewAuthenticationMiddleware(jwtService services.JWTService) AuthenticationMiddleware { + return &authenticationMiddleware{ + jwtService: jwtService, + } +} +func (m *authenticationMiddleware) VerifyAccount(c *gin.Context) { + + authorizationBearer := c.Request.Header["Authorization"] + + if authorizationBearer != nil { + token := strings.Split(authorizationBearer[0], " ")[1] + claim, err := m.jwtService.ValidateToken(c.Request.Context(), token) + + if err != nil && errors.Is(err, http_error.INVALID_TOKEN) { + utils.ResponseFAILED(c, claim, http_error.INVALID_TOKEN) + c.Abort() + return + } + fmt.Println("Claims:", claim) + c.Set("user_id", claim.UserId) + c.Next() + + } else { + utils.ResponseFAILED(c, "Empty Token", http_error.UNAUTHORIZED) + return + } + +} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..7ddf445b6250424f14f0025a76de0a8c274a18e5 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1 @@ +package middleware diff --git a/models/dto/authentication_dto.go b/models/dto/authentication_dto.go new file mode 100644 index 0000000000000000000000000000000000000000..c119e891a2c4092ab8c9cd32f2857a34a4530621 --- /dev/null +++ b/models/dto/authentication_dto.go @@ -0,0 +1,55 @@ +package dto + +import entity "abdanhafidz.com/go-boilerplate/models/entity" + +type SignInRequest struct { + EmailorUsername string `json:"email_or_username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type SignUpRequest struct { + Name string `json:"name"` + Email string `json:"email" binding:"required,email"` + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type CreateEmailVerificationRequest struct { + Email string `json:"email" binding:"required,email"` +} + +type ChangePasswordRequest struct { + OldPassword string `json:"old_password" binding:"required" ` + NewPassword string `json:"new_password" binding:"required"` +} + +type UpdateUserRoleRequest struct { + Role string `json:"role" binding:"required"` +} + +type ValidateVerifyEmailRequest struct { + Email string `json:"email" binding:"required,email"` + Token uint `json:"token" binding:"required"` +} + +type ExternalAuthRequest struct { + OauthID string `json:"oauth_id" binding:"required"` + OauthProvider string `json:"oauth_provider" binding:"required"` +} +type ResetPasswordRequest struct { + Token uint `json:"token" binding:"required"` + NewPassword string `json:"new_password" binding:"required"` +} +type ForgotPasswordRequest struct { + Email string `json:"email" binding:"required,email"` +} + +type ValidateForgotPasswordRequest struct { + Token uint `json:"token" binding:"required"` + NewPassword string `json:"new_password"` +} + +type AuthenticatedUser struct { + User entity.User `json:"user"` + Token string `json:"token"` +} diff --git a/models/dto/email_verification_dto.go b/models/dto/email_verification_dto.go new file mode 100644 index 0000000000000000000000000000000000000000..21766cd7d151bd8174ab1338e322b03f2546fa60 --- /dev/null +++ b/models/dto/email_verification_dto.go @@ -0,0 +1,5 @@ +package dto + +type DeleteEmailVerificationRequest struct { + Token uint `json:"token" binding:"required"` +} diff --git a/models/dto/home_dto.go b/models/dto/home_dto.go new file mode 100644 index 0000000000000000000000000000000000000000..9e16d2bdf247fe25032db6741e3f149d0012cf57 --- /dev/null +++ b/models/dto/home_dto.go @@ -0,0 +1,92 @@ +package dto + +type HomeResponse struct { + User HomeUser `json:"user"` + Notifications HomeNotifications `json:"notifications"` + Search HomeSearch `json:"search"` + HeroBanners []HomeHeroBanner `json:"hero_banners"` + PromoStrip HomePromoStrip `json:"promo_strip"` + WeekSchedule HomeWeekSchedule `json:"week_schedule"` + EventSections []HomeEventSection `json:"event_sections"` + BeginnerClassSections []HomeBeginnerClassSection `json:"beginner_class_sections"` +} + +type HomeUser struct { + ID string `json:"id"` + FirstName string `json:"first_name"` + FullName string `json:"full_name"` + AvatarUrl string `json:"avatar_url"` +} + +type HomeNotifications struct { + UnreadCount int `json:"unread_count"` +} + +type HomeSearch struct { + Placeholder string `json:"placeholder"` +} + +type HomeHeroBanner struct { + ID string `json:"id"` + Title string `json:"title"` + Subtitle string `json:"subtitle"` + ImageUrl string `json:"image_url"` + BadgeText string `json:"badge_text"` + CtaText string `json:"cta_text"` + CtaType string `json:"cta_type"` + CtaTargetID string `json:"cta_target_id"` +} + +type HomePromoStrip struct { + ID string `json:"id"` + Text string `json:"text"` + CtaText string `json:"cta_text"` + CtaType string `json:"cta_type"` + CtaTargetID string `json:"cta_target_id"` +} + +type HomeWeekSchedule struct { + WeekLabel string `json:"week_label"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + SelectedDate string `json:"selected_date"` + Days []HomeDay `json:"days"` + Items []ScheduleItem `json:"items"` + ViewAllUrl string `json:"view_all_url"` +} + +type HomeDay struct { + Date string `json:"date"` + DayNameShort string `json:"day_name_short"` + DayNumber int `json:"day_number"` + IsSelected bool `json:"is_selected"` + HasSchedule bool `json:"has_schedule"` +} + +type ScheduleItem struct { + ScheduleID string `json:"schedule_id"` + ClassID string `json:"class_id"` + Title string `json:"title"` + Date string `json:"date"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + DisplayTime string `json:"display_time"` + TeacherName string `json:"teacher_name"` + StudioName string `json:"studio_name"` + City string `json:"city"` + IsBooked bool `json:"is_booked"` +} + +type HomeEventSection struct { + ID string `json:"id"` + Name string `json:"name"` + ImageUrl string `json:"image_url"` + Type string `json:"type"` +} + +type HomeBeginnerClassSection struct { + ID string `json:"id"` + Name string `json:"name"` + ImageUrl string `json:"image_url"` + Type string `json:"type"` +} diff --git a/models/dto/home_swagger_wrapper.go b/models/dto/home_swagger_wrapper.go new file mode 100644 index 0000000000000000000000000000000000000000..aa81f5c3a0ca26c5d994bbadd6506640d845b812 --- /dev/null +++ b/models/dto/home_swagger_wrapper.go @@ -0,0 +1,18 @@ +package dto + +type HomeResponseWrapper struct { + Success bool `json:"success"` + Message string `json:"message"` + Data HomeResponse `json:"data"` +} + +type ScheduleListResponseWrapper struct { + Success bool `json:"success"` + Message string `json:"message"` + Data ScheduleListResponse `json:"data"` +} + +type CategoryResponseWrapper struct { + Success bool `json:"success"` + Data []CategoryResponse `json:"data"` +} diff --git a/models/dto/http_response_dto.go b/models/dto/http_response_dto.go new file mode 100644 index 0000000000000000000000000000000000000000..b091ea917c2d6090ddefdf202a58114d9e7ea441 --- /dev/null +++ b/models/dto/http_response_dto.go @@ -0,0 +1,15 @@ +package dto + +type SuccessResponse[TResponse any] struct { + Status string `json:"status"` + Data TResponse `json:"data"` + Message any `json:"message"` + MetaData any `json:"meta_data"` +} + +type ErrorResponse struct { + Status string `json:"status"` + Error error `json:"errors"` + Message any `json:"message"` + MetaData any `json:"meta_data"` +} diff --git a/models/dto/jwt_dto.go b/models/dto/jwt_dto.go new file mode 100644 index 0000000000000000000000000000000000000000..56791e06ecc822cdb7d549942bef97d34cd3e40b --- /dev/null +++ b/models/dto/jwt_dto.go @@ -0,0 +1,15 @@ +package dto + +import ( + "github.com/golang-jwt/jwt/v4" +) + +type JWTCustomClaims struct { + UserId string `json:"user_id" binding:"required"` + Role string `json:"role" binding:"required"` + jwt.RegisteredClaims +} + +type AccountData struct { + UserId string `json:"user_id" binding:"required"` +} diff --git a/models/dto/schedule_dto.go b/models/dto/schedule_dto.go new file mode 100644 index 0000000000000000000000000000000000000000..1363ab5d07ee9c91d2031685c4568273ef708eed --- /dev/null +++ b/models/dto/schedule_dto.go @@ -0,0 +1,45 @@ +package dto + +type ScheduleListResponse struct { + Items []ScheduleListItem `json:"items"` + Pagination PaginationMeta `json:"pagination"` +} + +type ScheduleListItem struct { + ScheduleID string `json:"schedule_id"` + ClassID string `json:"class_id"` + Title string `json:"title"` + Category string `json:"category"` + Level string `json:"level"` + Date string `json:"date"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + DisplayTime string `json:"display_time"` + TeacherName string `json:"teacher_name"` + StudioName string `json:"studio_name"` + Address string `json:"address"` + City string `json:"city"` + ThumbnailUrl string `json:"thumbnail_url"` + Price PriceData `json:"price"` + AvailableSlots int `json:"available_slots"` + IsBooked bool `json:"is_booked"` +} + +type PriceData struct { + Currency string `json:"currency"` + Amount int `json:"amount"` + Display string `json:"display"` +} + +type PaginationMeta struct { + Page int `json:"page"` + Limit int `json:"limit"` + TotalItems int `json:"total_items"` + TotalPages int `json:"total_pages"` +} + +type CategoryResponse struct { + ID string `json:"id"` + Name string `json:"name"` + ImageUrl string `json:"image_url"` +} diff --git a/models/entity/constant.go b/models/entity/constant.go new file mode 100644 index 0000000000000000000000000000000000000000..6c74a7600a5ad2e145ee798932340002b34714e7 --- /dev/null +++ b/models/entity/constant.go @@ -0,0 +1,32 @@ +package entities + +const ( + StatusNotStarted = "NOT_STARTED" + StatusInProgress = "IN_PROGRESS" + StatusFinished = "FINISHED" +) + +const ( + EventStatusUpcoming = "UPCOMING" + EventStatusOngoing = "ONGOING" + EventStatusEnded = "ENDED" +) + +const ( + PaymentStatusPending = "PENDING" + PaymentStatusPaid = "PAID" + PaymentStatusFailed = "FAILED" + PaymentStatusExpired = "EXPIRED" +) + +const MB = 1024 * 1024 + +type Pagination struct { + Limit int + Offset int + Search string + SortBy string + Order string + RegisterStatus *int + Status *string +} diff --git a/models/entity/entity.go b/models/entity/entity.go new file mode 100644 index 0000000000000000000000000000000000000000..17f6f7bd768b9a7d5e5fec7f83dbf33848a5d02d --- /dev/null +++ b/models/entity/entity.go @@ -0,0 +1,227 @@ +package entities + +import ( + "time" + + "gorm.io/datatypes" +) + +const ( + RoleMember = "member" + RoleInstructor = "instructor" + RoleVenueOwner = "venue_owner" + RoleAdmin = "admin" +) + +type User struct { + ID string `gorm:"type:uuid;primaryKey"` + PhoneNumber string `gorm:"uniqueIndex;not null"` + Email *string + PasswordHash *string + FullName string + Nickname string + Gender *string + DanceLevel *string + Province *string + City *string + Description *string `gorm:"type:text"` + Interests datatypes.JSON + ProfileMedia *string + PrefedLanguage *string + AdditionalDetails datatypes.JSON + + IsMember bool + IsInstructor bool + IsVO bool + + CreatedAt time.Time + UpdatedAt time.Time + + Roles []UserRole `gorm:"foreignKey:UserID"` +} + +type UserRole struct { + ID string `gorm:"type:uuid;primaryKey"` + UserID string `gorm:"type:uuid;index"` + Role string `gorm:"type:varchar(50)"` + CreatedAt time.Time + + User User `gorm:"foreignKey:UserID"` +} + +type Instructor struct { + ID string `gorm:"type:uuid;primaryKey"` + UserID string `gorm:"type:uuid;uniqueIndex"` + Rank string + Specialization datatypes.JSON + Introduction datatypes.JSON + ProfileMedia datatypes.JSON + + CreatedAt time.Time + + User User `gorm:"foreignKey:UserID"` +} + +type VenueOwner struct { + ID string `gorm:"type:uuid;primaryKey"` + UserID string `gorm:"type:uuid;uniqueIndex"` + CreatedAt time.Time + + User User `gorm:"foreignKey:UserID"` +} + +type Venue struct { + ID string `gorm:"type:uuid;primaryKey"` + OwnerID string `gorm:"type:uuid;index"` + Name string + Description datatypes.JSON + Province string + City string + Address string + Latitude float64 + Longitude float64 + Capacity int + MinimumConsumption float64 + OpeningTime string + ClosingTime string + VenueMedia datatypes.JSON + VerificationStatus string + + CreatedAt time.Time + + Owner VenueOwner `gorm:"foreignKey:OwnerID"` +} + +type Class struct { + ID string `gorm:"type:uuid;primaryKey"` + InstructorID string `gorm:"type:uuid;index"` + VenueID *string `gorm:"type:uuid"` + + Title string + Description datatypes.JSON + Capacity int + Price float64 + + ScheduleType string + StartTime time.Time + EndTime time.Time + + ClassMedia datatypes.JSON + + CreatedAt time.Time + + Instructor Instructor `gorm:"foreignKey:InstructorID"` + Venue *Venue `gorm:"foreignKey:VenueID"` +} + +type ClassBooking struct { + ID string `gorm:"type:uuid;primaryKey"` + ClassID string `gorm:"type:uuid;index"` + UserID string `gorm:"type:uuid;index"` + BookingStatus string + PaymentStatus string + PaymentMethod string + + CreatedAt time.Time + + Class Class `gorm:"foreignKey:ClassID"` + User User `gorm:"foreignKey:UserID"` +} + +type Event struct { + ID string `gorm:"type:uuid;primaryKey"` + HostUserID string `gorm:"type:uuid;index"` + VenueID string `gorm:"type:uuid"` + + Title string + Description datatypes.JSON + + ScheduleType string + EventDate time.Time + + StartTime time.Time + EndTime time.Time + + Capacity int + EntranceFee float64 + + EventMedia datatypes.JSON + + CreatedAt time.Time + + HostUser User `gorm:"foreignKey:HostUserID"` + Venue Venue `gorm:"foreignKey:VenueID"` +} + +type EventInstructor struct { + ID string `gorm:"type:uuid;primaryKey"` + EventID string `gorm:"type:uuid;index"` + InstructorID string `gorm:"type:uuid;index"` + PartnerCapacity int + + CreatedAt time.Time + + Event Event `gorm:"foreignKey:EventID"` + Instructor Instructor `gorm:"foreignKey:InstructorID"` +} + +type EventPackage struct { + ID string `gorm:"type:uuid;primaryKey"` + EventID string `gorm:"type:uuid;index"` + Name string + Description datatypes.JSON + Price float64 + PartnerIncluded bool + + CreatedAt time.Time + + Event Event `gorm:"foreignKey:EventID"` +} + +type EventBooking struct { + ID string `gorm:"type:uuid;primaryKey"` + EventID string `gorm:"type:uuid;index"` + PackageID string `gorm:"type:uuid"` + UserID string `gorm:"type:uuid"` + PartnerInstructorID *string `gorm:"type:uuid"` + + BookingStatus string + PaymentStatus string + PaymentMethod string + + CreatedAt time.Time + + Event Event `gorm:"foreignKey:EventID"` + Package EventPackage `gorm:"foreignKey:PackageID"` + User User `gorm:"foreignKey:UserID"` +} + +type Payment struct { + ID string `gorm:"type:uuid;primaryKey"` + + UserID string `gorm:"type:uuid"` + + EventBookingID *string `gorm:"type:uuid"` + ClassBookingID *string `gorm:"type:uuid"` + + Amount float64 + PaymentMethod string + PaymentStatus string + + XenditInvoiceID *string + + CreatedAt time.Time + + User User `gorm:"foreignKey:UserID"` +} + +type OTPVerification struct { + ID string `gorm:"type:uuid;primaryKey"` + PhoneNumber string + OTPCode string + + ExpiredAt time.Time + Verified bool + + CreatedAt time.Time +} diff --git a/models/error/error.go b/models/error/error.go new file mode 100644 index 0000000000000000000000000000000000000000..4c88c58861052e2c5c5634c2ac92aeae819797f2 --- /dev/null +++ b/models/error/error.go @@ -0,0 +1,69 @@ +package http_error + +import "errors" + +var ( + // ================= GENERAL ================= + BAD_REQUEST_ERROR = errors.New("Invalid request format") + INTERNAL_SERVER_ERROR = errors.New("Internal server error") + TIMEOUT = errors.New("Server took too long to respond") + NOT_FOUND_ERROR = errors.New("Resource not found") + DUPLICATE_DATA = errors.New("Duplicate data") + INVALID_DATA_PAYLOAD = errors.New("Invalid data payload provided") + DATA_NOT_FOUND = errors.New("Data not found") + FORBIDDEN_ERROR = errors.New("Forbidden, you don't have permission to access this service") + + // ================= AUTH & ACCOUNT ================= + UNAUTHORIZED = errors.New("Unauthorized, you don't have permission to access this service") + EXISTING_ACCOUNT = errors.New("Account already exists") + INVALID_TOKEN = errors.New("Invalid authentication payload") + ACCOUNT_NOT_FOUND = errors.New("There is no account with the given credentials") + WRONG_PASSWORD = errors.New("Invalid password, please check your credentials") + INVALID_ACCOUNT_DIGITS = errors.New("Your account 3 digits is not found in account number data") + EXPIRED_TOKEN = errors.New("Token expired") + INVALID_OTP = errors.New("Invalid OTP code") + EMAIL_ALREADY_EXISTS = errors.New("Email already registered") + + // ================= EVENT & EXAM ================= + ALREADY_REGISTERED_TO_EVENT = errors.New("Account already registered to this event") + NOT_REGISTERED_TO_EVENT = errors.New("Account is not registered to this event") + ERR_PROBLEM_SET_NOT_FOUND = errors.New("Problem set not found") + ERR_QUESTION_NOT_FOUND = errors.New("Question not found") + EVENT_FINISHED = errors.New("The event has ended, you are disallowed to take the exam") + EVENT_NOT_STARTED = errors.New("Take it easy, event hasn't started yet! You cannot take the exam") + EXAMS_SUBMITTED = errors.New("You have submitted the exam, you are disallowed to answer the question") + EXAMS_TIME_EXCEEDED = errors.New("Time limit exceeded for this attempt") + IMAGE_REQUIRED = errors.New("Image is required") + DESCRIPTION_REQUIRED = errors.New("Description is required") + CODE_REQUIRED = errors.New("Code is required") + INVALID_CODE = errors.New("Code must be on range 6-12") + EVENT_START_DATE_INVALID = errors.New("Event start date must be in the future") + EVENT_END_DATE_INVALID = errors.New("Event end date must be after start date") + INVALID_DATE_FORMAT = errors.New("Invalid date format, please use RFC3339") + EVENT_START_DATE_IN_PAST = errors.New("Event start date cannot be in the past") + + // ================= FILE UPLOAD ================= + FILE_TOO_LARGE = errors.New("File size exceeds the maximum limit") + INVALID_FILE_TYPE = errors.New("File type is not permitted for the selected context") + UPLOAD_FAILED = errors.New("Failed to upload file to storage provider") + PARTIAL_UPLOAD_FAILURE = errors.New("Some files failed validation or upload") + INVALID_UPLOAD_CONTEXT_ERROR = errors.New("Invalid upload context") + + // ================= ACADEMY ================= + TITLE_REQUIRED = errors.New("Title cannot be empty") + SLUG_REQUIRED = errors.New("Slug cannot be empty") + ACADEMY_ID_REQUIRED = errors.New("Academy ID is required") + MATERIAL_ID_REQUIRED = errors.New("Material ID is required") + + ACADEMY_NOT_FOUND = errors.New("Academy not found") + MATERIAL_NOT_FOUND = errors.New("Material not found") + CONTENT_NOT_FOUND = errors.New("Content not found") + ACADEMY_HAS_MATERIALS = errors.New("Cannot delete academy because it still has materials") + MATERIAL_HAS_CONTENTS = errors.New("Cannot delete material because it still has contents") + + PROBLEM_SET_NOT_FOUND = errors.New("problem set not found") + QUESTION_NOT_FOUND = errors.New("question not found") + + PAYMENT_FAILED = errors.New("There is error during payment process try again later!") + PAYMENT_REQUIRED = errors.New("Payment is required to access this content") +) diff --git a/provider/config_provider.go b/provider/config_provider.go new file mode 100644 index 0000000000000000000000000000000000000000..f16212f6783ce63c9dfa8e276a5f7df8a66bc477 --- /dev/null +++ b/provider/config_provider.go @@ -0,0 +1,64 @@ +package provider + +import ( + "abdanhafidz.com/go-boilerplate/config" +) + +type ConfigProvider interface { + ProvideDatabaseConfig() config.DatabaseConfig + ProvideEnvConfig() config.EnvConfig + ProvideUploadConfig() config.UploadConfig + ProvideSupabaseConfig() config.SupabaseConfig + ProvideJWTConfig() config.JWTConfig + ProvideXenditConfig() config.XenditConfig +} + +type configProvider struct { + databaseConfig config.DatabaseConfig + envConfig config.EnvConfig + uploadConfig config.UploadConfig + supabaseConfig config.SupabaseConfig + jWTConfig config.JWTConfig + xenditConfig config.XenditConfig +} + +func NewConfigProvider() ConfigProvider { + envConfig := config.NewEnvConfig("Asia / Jakarta") + databaseConfig := config.NewDatabaseConfig(envConfig.GetDatabaseHost(), envConfig.GetDatabaseUser(), envConfig.GetDatabasePassword(), envConfig.GetDatabaseName(), envConfig.GetDatabasePort()) + uploadConfig := config.NewUploadConfig() + supabaseConfig := config.NewSupabaseConfig(envConfig.GetSupabaseURL(), envConfig.GetSupabaseKey(), envConfig.GetSupabaseBucket()) + jWTConfig := config.NewJWTConfig(envConfig.GetSalt()) + xenditConfig := config.NewXenditConfig(envConfig) + return &configProvider{ + databaseConfig: databaseConfig, + envConfig: envConfig, + uploadConfig: uploadConfig, + supabaseConfig: supabaseConfig, + jWTConfig: jWTConfig, + xenditConfig: xenditConfig, + } +} + +func (c *configProvider) ProvideDatabaseConfig() config.DatabaseConfig { + return c.databaseConfig +} + +func (c *configProvider) ProvideEnvConfig() config.EnvConfig { + return c.envConfig +} + +func (c *configProvider) ProvideUploadConfig() config.UploadConfig { + return c.uploadConfig +} + +func (c *configProvider) ProvideSupabaseConfig() config.SupabaseConfig { + return c.supabaseConfig +} + +func (c *configProvider) ProvideJWTConfig() config.JWTConfig { + return c.jWTConfig +} + +func (c *configProvider) ProvideXenditConfig() config.XenditConfig { + return c.xenditConfig +} diff --git a/provider/controller_provider.go b/provider/controller_provider.go new file mode 100644 index 0000000000000000000000000000000000000000..c3b58eee1eb3edb27861bed77206554722bf8b94 --- /dev/null +++ b/provider/controller_provider.go @@ -0,0 +1,47 @@ +package provider + +import "abdanhafidz.com/go-boilerplate/controllers" + +type ControllerProvider interface { + ProvideAuthenticationController() controllers.AuthenticationController + ProvideOTPVerificationController() controllers.OTPVerificationController + ProvidePaymentCallbackController() controllers.PaymentCallbackController + ProvideHomeController() controllers.HomeController +} + +type controllerProvider struct { + authenticationController controllers.AuthenticationController + otpVerificationController controllers.OTPVerificationController + paymentCallbackController controllers.PaymentCallbackController + homeController controllers.HomeController +} + +func NewControllerProvider(servicesProvider ServicesProvider) ControllerProvider { + authenticationController := controllers.NewAuthenticationController(servicesProvider.ProvideUserService()) + otpVerificationController := controllers.NewOTPVerificationController(servicesProvider.ProvideOTPVerificationService()) + paymentCallbackController := controllers.NewPaymentCallbackController(servicesProvider.ProvidePaymentService()) + homeController := controllers.NewHomeController(servicesProvider.ProvideHomeService()) + + return &controllerProvider{ + authenticationController: authenticationController, + otpVerificationController: otpVerificationController, + paymentCallbackController: paymentCallbackController, + homeController: homeController, + } +} + +func (c *controllerProvider) ProvideAuthenticationController() controllers.AuthenticationController { + return c.authenticationController +} + +func (c *controllerProvider) ProvideOTPVerificationController() controllers.OTPVerificationController { + return c.otpVerificationController +} + +func (c *controllerProvider) ProvidePaymentCallbackController() controllers.PaymentCallbackController { + return c.paymentCallbackController +} + +func (c *controllerProvider) ProvideHomeController() controllers.HomeController { + return c.homeController +} diff --git a/provider/middleware_provider.go b/provider/middleware_provider.go new file mode 100644 index 0000000000000000000000000000000000000000..0ac4ccb73341e50a454787d5aedac34eac350860 --- /dev/null +++ b/provider/middleware_provider.go @@ -0,0 +1,22 @@ +package provider + +import "abdanhafidz.com/go-boilerplate/middleware" + +type MiddlewareProvider interface { + ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware +} + +type middlewareProvider struct { + authenticationMiddleware middleware.AuthenticationMiddleware +} + +func NewMiddlewareProvider(servicesProvider ServicesProvider) MiddlewareProvider { + authenticationMiddleware := middleware.NewAuthenticationMiddleware(servicesProvider.ProvideJWTService()) + return &middlewareProvider{ + authenticationMiddleware: authenticationMiddleware, + } +} + +func (p *middlewareProvider) ProvideAuthenticationMiddleware() middleware.AuthenticationMiddleware { + return p.authenticationMiddleware +} diff --git a/provider/provider.go b/provider/provider.go new file mode 100644 index 0000000000000000000000000000000000000000..ce6b824c9d0491ce362a4bb647872182280f7396 --- /dev/null +++ b/provider/provider.go @@ -0,0 +1,111 @@ +package provider + +import ( + "log" + + entity "abdanhafidz.com/go-boilerplate/models/entity" + "github.com/gin-gonic/gin" +) + +type AppProvider interface { + ProvideRouter() *gin.Engine + ProvideConfig() ConfigProvider + ProvideRepositories() RepositoriesProvider + ProvideServices() ServicesProvider + ProvideControllers() ControllerProvider + ProvideMiddlewares() MiddlewareProvider +} +type appProvider struct { + ginRouter *gin.Engine + configProvider ConfigProvider + repositoriesProvider RepositoriesProvider + servicesProvider ServicesProvider + controllerProvider ControllerProvider + middlewareProvider MiddlewareProvider +} + +func NewAppProvider() AppProvider { + log.Println("[BOOT] Initializing App Provider...") + + log.Println("[BOOT] Creating Gin Router") + ginRouter := gin.Default() + + log.Println("[BOOT] Initializing Config Provider") + configProvider := NewConfigProvider() + + log.Println("[BOOT] Initializing Repositories Provider") + repositoriesProvider := NewRepositoriesProvider(configProvider) + + log.Println("[BOOT] Initializing Services Provider") + servicesProvider := NewServicesProvider(repositoriesProvider, configProvider) + + log.Println("[BOOT] Initializing Controller Provider") + controllerProvider := NewControllerProvider(servicesProvider) + + log.Println("[BOOT] Initializing Middleware Provider") + middlewareProvider := NewMiddlewareProvider(servicesProvider) + + // =============================== + // DATABASE MIGRATION + // =============================== + log.Println("[BOOT][DB] Starting database migration...") + + dbConfig := configProvider.ProvideDatabaseConfig() + log.Println("[BOOT][DB] Database config acquired") + + err := dbConfig.AutoMigrateAll( + &entity.User{}, + &entity.UserRole{}, + &entity.Instructor{}, + &entity.VenueOwner{}, + &entity.Venue{}, + &entity.Class{}, + &entity.ClassBooking{}, + &entity.Event{}, + &entity.EventInstructor{}, + &entity.EventPackage{}, + &entity.EventBooking{}, + &entity.Payment{}, + &entity.OTPVerification{}, + ) + + if err != nil { + log.Fatalf("[BOOT][DB] ❌ Database migration failed: %v", err) + } + + log.Println("[BOOT][DB] ✅ Database migration completed") + + log.Println("[BOOT] App Provider initialized successfully") + + return &appProvider{ + ginRouter: ginRouter, + configProvider: configProvider, + repositoriesProvider: repositoriesProvider, + servicesProvider: servicesProvider, + controllerProvider: controllerProvider, + middlewareProvider: middlewareProvider, + } +} + +func (a *appProvider) ProvideRouter() *gin.Engine { + return a.ginRouter +} +func (a *appProvider) ProvideConfig() ConfigProvider { + return a.configProvider +} + +func (a *appProvider) ProvideRepositories() RepositoriesProvider { + return a.repositoriesProvider +} + +func (a *appProvider) ProvideServices() ServicesProvider { + return a.servicesProvider +} + +func (a *appProvider) ProvideControllers() ControllerProvider { + return a.controllerProvider +} + +func (a *appProvider) ProvideMiddlewares() MiddlewareProvider { + return a.middlewareProvider +} diff --git a/provider/repositories_provider.go b/provider/repositories_provider.go new file mode 100644 index 0000000000000000000000000000000000000000..cd723b4f8605cd6b20b41b91bf9045642ee457a2 --- /dev/null +++ b/provider/repositories_provider.go @@ -0,0 +1,42 @@ +package provider + +import "abdanhafidz.com/go-boilerplate/repositories" + +type RepositoriesProvider interface { + ProvideUserRepository() repositories.UserRepository + ProvideOTPVerificationRepository() repositories.OTPVerificationRepository + ProvideHomeRepository() repositories.HomeRepository +} + +type repositoriesProvider struct { + userRepository repositories.UserRepository + otpVerificationRepository repositories.OTPVerificationRepository + homeRepository repositories.HomeRepository +} + +func NewRepositoriesProvider(cfg ConfigProvider) RepositoriesProvider { + dbConfig := cfg.ProvideDatabaseConfig() + db := dbConfig.GetInstance() + + userRepository := repositories.NewUserRepository(db) + otpVerificationRepository := repositories.NewOTPVerificationRepository(db) + homeRepository := repositories.NewHomeRepository(db) + + return &repositoriesProvider{ + userRepository: userRepository, + otpVerificationRepository: otpVerificationRepository, + homeRepository: homeRepository, + } +} + +func (r *repositoriesProvider) ProvideUserRepository() repositories.UserRepository { + return r.userRepository +} + +func (r *repositoriesProvider) ProvideOTPVerificationRepository() repositories.OTPVerificationRepository { + return r.otpVerificationRepository +} + +func (r *repositoriesProvider) ProvideHomeRepository() repositories.HomeRepository { + return r.homeRepository +} diff --git a/provider/services_provider.go b/provider/services_provider.go new file mode 100644 index 0000000000000000000000000000000000000000..cd987bd0f020011e53321a432518a96624c26959 --- /dev/null +++ b/provider/services_provider.go @@ -0,0 +1,58 @@ +package provider + +import ( + "abdanhafidz.com/go-boilerplate/services" +) + +type ServicesProvider interface { + ProvideJWTService() services.JWTService + ProvidePaymentService() services.PaymentService + ProvideUserService() services.UserService + ProvideOTPVerificationService() services.OTPVerificationService + ProvideHomeService() services.HomeService +} + +type servicesProvider struct { + jWTService services.JWTService + paymentService services.PaymentService + userService services.UserService + otpVerificationService services.OTPVerificationService + homeService services.HomeService +} + +func NewServicesProvider(repoProvider RepositoriesProvider, configProvider ConfigProvider) ServicesProvider { + jWTService := services.NewJWTService(configProvider.ProvideJWTConfig().GetSecretKey()) + paymentService := services.NewPaymentService(configProvider.ProvideXenditConfig().GetClient()) + + userService := services.NewUserService(jWTService, repoProvider.ProvideUserRepository()) + otpVerificationService := services.NewOTPVerificationService(userService, repoProvider.ProvideOTPVerificationRepository()) + homeService := services.NewHomeService(repoProvider.ProvideHomeRepository()) + + return &servicesProvider{ + jWTService: jWTService, + paymentService: paymentService, + userService: userService, + otpVerificationService: otpVerificationService, + homeService: homeService, + } +} + +func (s *servicesProvider) ProvideJWTService() services.JWTService { + return s.jWTService +} + +func (s *servicesProvider) ProvidePaymentService() services.PaymentService { + return s.paymentService +} + +func (s *servicesProvider) ProvideUserService() services.UserService { + return s.userService +} + +func (s *servicesProvider) ProvideOTPVerificationService() services.OTPVerificationService { + return s.otpVerificationService +} + +func (s *servicesProvider) ProvideHomeService() services.HomeService { + return s.homeService +} diff --git a/repositories/home_repository.go b/repositories/home_repository.go new file mode 100644 index 0000000000000000000000000000000000000000..5c528a68e40ac3d51d33d1da12479afddf12a34a --- /dev/null +++ b/repositories/home_repository.go @@ -0,0 +1,158 @@ +package repositories + +import ( + "context" + + "abdanhafidz.com/go-boilerplate/models/dto" + "gorm.io/gorm" +) + +type HomeRepository interface { + GetHomeData(ctx context.Context, userId string, params map[string]interface{}) (dto.HomeResponse, error) + GetSchedules(ctx context.Context, userId string, params map[string]interface{}) (dto.ScheduleListResponse, error) + GetEventCategories(ctx context.Context) ([]dto.CategoryResponse, error) + GetClassCategories(ctx context.Context) ([]dto.CategoryResponse, error) +} + +type homeRepository struct { + db *gorm.DB +} + +func NewHomeRepository(db *gorm.DB) HomeRepository { + return &homeRepository{db: db} +} + +func (r *homeRepository) GetHomeData(ctx context.Context, userId string, params map[string]interface{}) (dto.HomeResponse, error) { + // For now, returning hardcoded dummy data as requested by FE + return dto.HomeResponse{ + User: dto.HomeUser{ + ID: "usr_001", + FirstName: "John", + FullName: "John Doe", + AvatarUrl: "https://cdn.danzaapp.com/avatar/usr_001.jpg", + }, + Notifications: dto.HomeNotifications{UnreadCount: 5}, + Search: dto.HomeSearch{Placeholder: "What are you up to?"}, + HeroBanners: []dto.HomeHeroBanner{ + { + ID: "bnr_001", + Title: "Dance Show", + Subtitle: "April 8th 10:00PM", + ImageUrl: "https://cdn.danzaapp.com/banners/dance-show.png", + BadgeText: "Exclusive", + CtaText: "View Event", + CtaType: "event", + CtaTargetID: "evt_101", + }, + }, + PromoStrip: dto.HomePromoStrip{ + ID: "promo_001", + Text: "Psst.. Get 50% discount off this thursday!", + CtaText: "See Promo", + CtaType: "promotion", + CtaTargetID: "prm_001", + }, + WeekSchedule: dto.HomeWeekSchedule{ + WeekLabel: "This Week", + StartDate: "2026-03-12", + EndDate: "2026-03-18", + SelectedDate: "2026-03-13", + Days: []dto.HomeDay{ + {Date: "2026-03-12", DayNameShort: "Sun", DayNumber: 12, IsSelected: false, HasSchedule: false}, + {Date: "2026-03-13", DayNameShort: "Mon", DayNumber: 13, IsSelected: true, HasSchedule: true}, + {Date: "2026-03-14", DayNameShort: "Tue", DayNumber: 14, IsSelected: false, HasSchedule: false}, + }, + Items: []dto.ScheduleItem{ + { + ScheduleID: "sch_001", + ClassID: "cls_201", + Title: "Contemporary Salsa", + Date: "2026-03-13", + StartTime: "17:00", + EndTime: "18:30", + DisplayTime: "17.00-18.30", + TeacherName: "Ariana", + StudioName: "Danza Studio A", + City: "Bogor", + IsBooked: false, + }, + { + ScheduleID: "sch_002", + ClassID: "cls_202", + Title: "Hip Hop Dance Bogor", + Date: "2026-03-18", + StartTime: "17:00", + EndTime: "18:30", + DisplayTime: "17.00-18.30", + TeacherName: "Rico", + StudioName: "Danza Studio B", + City: "Bogor", + IsBooked: true, + }, + }, + ViewAllUrl: "/v1/schedules", + }, + EventSections: []dto.HomeEventSection{ + {ID: "cat_evt_popular", Name: "Popular", ImageUrl: "https://cdn.danzaapp.com/categories/popular.png", Type: "event_category"}, + {ID: "cat_evt_salsa", Name: "Salsa", ImageUrl: "https://cdn.danzaapp.com/categories/salsa.png", Type: "event_category"}, + {ID: "cat_evt_ballroom", Name: "Ballroom", ImageUrl: "https://cdn.danzaapp.com/categories/ballroom.png", Type: "event_category"}, + {ID: "cat_evt_tango", Name: "Tango", ImageUrl: "https://cdn.danzaapp.com/categories/tango.png", Type: "event_category"}, + }, + BeginnerClassSections: []dto.HomeBeginnerClassSection{ + {ID: "cat_cls_nearby", Name: "Nearby", ImageUrl: "https://cdn.danzaapp.com/categories/nearby.png", Type: "class_category"}, + {ID: "cat_cls_ballroom", Name: "Ballroom", ImageUrl: "https://cdn.danzaapp.com/categories/ballroom.png", Type: "class_category"}, + {ID: "cat_cls_tango", Name: "Tango", ImageUrl: "https://cdn.danzaapp.com/categories/tango.png", Type: "class_category"}, + }, + }, nil +} + +func (r *homeRepository) GetSchedules(ctx context.Context, userId string, params map[string]interface{}) (dto.ScheduleListResponse, error) { + // For now, returning hardcoded dummy data + return dto.ScheduleListResponse{ + Items: []dto.ScheduleListItem{ + { + ScheduleID: "sch_001", + ClassID: "cls_201", + Title: "Contemporary Salsa", + Category: "Salsa", + Level: "Beginner", + Date: "2026-03-13", + StartTime: "17:00", + EndTime: "18:30", + DisplayTime: "17.00-18.30", + TeacherName: "Ariana", + StudioName: "Danza Studio A", + Address: "Jl. Merdeka No. 10, Bogor", + City: "Bogor", + ThumbnailUrl: "https://cdn.danzaapp.com/classes/cls_201.png", + Price: dto.PriceData{ + Currency: "IDR", + Amount: 100000, + Display: "Rp100.000", + }, + AvailableSlots: 12, + IsBooked: false, + }, + }, + Pagination: dto.PaginationMeta{ + Page: 1, + Limit: 10, + TotalItems: 1, + TotalPages: 1, + }, + }, nil +} + +func (r *homeRepository) GetEventCategories(ctx context.Context) ([]dto.CategoryResponse, error) { + return []dto.CategoryResponse{ + {ID: "cat_evt_popular", Name: "Popular", ImageUrl: "https://cdn.danzaapp.com/categories/popular.png"}, + {ID: "cat_evt_salsa", Name: "Salsa", ImageUrl: "https://cdn.danzaapp.com/categories/salsa.png"}, + }, nil +} + +func (r *homeRepository) GetClassCategories(ctx context.Context) ([]dto.CategoryResponse, error) { + return []dto.CategoryResponse{ + {ID: "cat_cls_nearby", Name: "Nearby", ImageUrl: "https://cdn.danzaapp.com/categories/nearby.png"}, + {ID: "cat_cls_tango", Name: "Tango", ImageUrl: "https://cdn.danzaapp.com/categories/tango.png"}, + }, nil +} diff --git a/repositories/otp_verification_repository.go b/repositories/otp_verification_repository.go new file mode 100644 index 0000000000000000000000000000000000000000..5494eca8a6354e222fbd1f94ff1caf09ad7eeb3f --- /dev/null +++ b/repositories/otp_verification_repository.go @@ -0,0 +1,72 @@ +package repositories + +import ( + "context" + "time" + + entity "abdanhafidz.com/go-boilerplate/models/entity" + "gorm.io/gorm" +) + +type OTPVerificationRepository interface { + Create(ctx context.Context, verification entity.OTPVerification) (entity.OTPVerification, error) + GetByPhoneNumberAndCode(ctx context.Context, phoneNumber string, code string) (entity.OTPVerification, error) + MarkVerified(ctx context.Context, id string) error + DeleteByPhoneNumber(ctx context.Context, phoneNumber string) error + GetActiveByPhoneNumber(ctx context.Context, phoneNumber string) ([]entity.OTPVerification, error) + ExpireAllOverdue(ctx context.Context, now time.Time) (int64, error) +} + +type otpVerificationRepository struct { + db *gorm.DB +} + +func NewOTPVerificationRepository(db *gorm.DB) OTPVerificationRepository { + return &otpVerificationRepository{db: db} +} + +func (r *otpVerificationRepository) Create(ctx context.Context, verification entity.OTPVerification) (entity.OTPVerification, error) { + if err := r.db.WithContext(ctx).Create(&verification).Error; err != nil { + return entity.OTPVerification{}, err + } + return verification, nil +} + +func (r *otpVerificationRepository) GetByPhoneNumberAndCode(ctx context.Context, phoneNumber string, code string) (entity.OTPVerification, error) { + var ev entity.OTPVerification + if err := r.db.WithContext(ctx). + Where("phone_number = ? AND otp_code = ? AND verified = ?", phoneNumber, code, false). + First(&ev).Error; err != nil { + return entity.OTPVerification{}, err + } + return ev, nil +} + +func (r *otpVerificationRepository) MarkVerified(ctx context.Context, id string) error { + return r.db.WithContext(ctx). + Model(&entity.OTPVerification{}). + Where("id = ?", id). + Update("verified", true).Error +} + +func (r *otpVerificationRepository) DeleteByPhoneNumber(ctx context.Context, phoneNumber string) error { + return r.db.WithContext(ctx).Where("phone_number = ?", phoneNumber).Delete(&entity.OTPVerification{}).Error +} + +func (r *otpVerificationRepository) GetActiveByPhoneNumber(ctx context.Context, phoneNumber string) ([]entity.OTPVerification, error) { + var list []entity.OTPVerification + if err := r.db.WithContext(ctx). + Where("phone_number = ? AND expired_at > ? AND verified = ?", phoneNumber, time.Now(), false). + Find(&list).Error; err != nil { + return nil, err + } + return list, nil +} + +func (r *otpVerificationRepository) ExpireAllOverdue(ctx context.Context, now time.Time) (int64, error) { + tx := r.db.WithContext(ctx). + Model(&entity.OTPVerification{}). + Where("verified = ? AND expired_at <= ?", false, now). + Update("verified", true) + return tx.RowsAffected, tx.Error +} diff --git a/repositories/repository.go b/repositories/repository.go new file mode 100644 index 0000000000000000000000000000000000000000..3f43206c7593b67dd66635cc00d1b125d511b40c --- /dev/null +++ b/repositories/repository.go @@ -0,0 +1 @@ +package repositories diff --git a/repositories/user_repository.go b/repositories/user_repository.go new file mode 100644 index 0000000000000000000000000000000000000000..14fc8a827f44301643873b12c8a1ed5dc919f7d9 --- /dev/null +++ b/repositories/user_repository.go @@ -0,0 +1,76 @@ +package repositories + +import ( + "context" + + entity "abdanhafidz.com/go-boilerplate/models/entity" + "gorm.io/gorm" +) + +type UserRepository interface { + CreateUser(ctx context.Context, user entity.User) (entity.User, error) + GetUserById(ctx context.Context, id string) (entity.User, error) + GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (entity.User, error) + GetUserByEmail(ctx context.Context, email string) (entity.User, error) + GetAllUsers(ctx context.Context) ([]entity.User, error) + UpdateUser(ctx context.Context, user entity.User) (entity.User, error) + DeleteUser(ctx context.Context, id string) error +} + +type userRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{db: db} +} + +func (r *userRepository) CreateUser(ctx context.Context, user entity.User) (entity.User, error) { + if err := r.db.WithContext(ctx).Create(&user).Error; err != nil { + return entity.User{}, err + } + return user, nil +} + +func (r *userRepository) GetUserById(ctx context.Context, id string) (entity.User, error) { + var user entity.User + if err := r.db.WithContext(ctx).First(&user, "id = ?", id).Preload("Roles").Error; err != nil { + return entity.User{}, err + } + return user, nil +} + +func (r *userRepository) GetUserByPhoneNumber(ctx context.Context, phoneNumber string) (entity.User, error) { + var user entity.User + if err := r.db.WithContext(ctx).First(&user, "phone_number = ?", phoneNumber).Preload("Roles").Error; err != nil { + return entity.User{}, err + } + return user, nil +} + +func (r *userRepository) GetUserByEmail(ctx context.Context, email string) (entity.User, error) { + var user entity.User + if err := r.db.WithContext(ctx).First(&user, "email = ?", email).Preload("Roles").Error; err != nil { + return entity.User{}, err + } + return user, nil +} + +func (r *userRepository) GetAllUsers(ctx context.Context) ([]entity.User, error) { + var users []entity.User + if err := r.db.WithContext(ctx).Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +func (r *userRepository) UpdateUser(ctx context.Context, user entity.User) (entity.User, error) { + if err := r.db.WithContext(ctx).Save(&user).Error; err != nil { + return entity.User{}, err + } + return user, nil +} + +func (r *userRepository) DeleteUser(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&entity.User{}, "id = ?", id).Error +} diff --git a/router/authentication_router.go b/router/authentication_router.go new file mode 100644 index 0000000000000000000000000000000000000000..8829f7d9a83536738877196cf5392a7a08248bbc --- /dev/null +++ b/router/authentication_router.go @@ -0,0 +1,20 @@ +package router + +import ( + "abdanhafidz.com/go-boilerplate/provider" + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" +) + +func AuthenticationRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) { + routerGroup := router.Group("/api/v1/authentication") + authenticationController := controller.ProvideAuthenticationController() + authenticationmiddleware := middleware.ProvideAuthenticationMiddleware() + + routerGroup.Use(gzip.Gzip(gzip.DefaultCompression)) + { + routerGroup.POST("/login", authenticationController.SignIn) + routerGroup.POST("/register", authenticationController.SignUp) + routerGroup.PUT("/change-password", authenticationmiddleware.VerifyAccount, authenticationController.ChangePassword) + } +} diff --git a/router/home_router.go b/router/home_router.go new file mode 100644 index 0000000000000000000000000000000000000000..3ec22a29ce2ee69f5cc8a33e8b7cc4a4337a58e8 --- /dev/null +++ b/router/home_router.go @@ -0,0 +1,26 @@ +package router + +import ( + "abdanhafidz.com/go-boilerplate/provider" + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" +) + +func HomeRouter(router *gin.Engine, middleware provider.MiddlewareProvider, controller provider.ControllerProvider) { + routerGroup := router.Group("/api/v1") + homeController := controller.ProvideHomeController() + authMiddleware := middleware.ProvideAuthenticationMiddleware() + + routerGroup.Use(gzip.Gzip(gzip.DefaultCompression)) + { + // Requires Authentication + protected := routerGroup.Group("/") + protected.Use(authMiddleware.VerifyAccount) + protected.GET("/home", homeController.GetHomeData) + protected.GET("/schedules", homeController.GetSchedules) + + // Public Endpoints (Master Data) + routerGroup.GET("/event-categories", homeController.GetEventCategories) + routerGroup.GET("/class-categories", homeController.GetClassCategories) + } +} diff --git a/router/otp_verification_router.go b/router/otp_verification_router.go new file mode 100644 index 0000000000000000000000000000000000000000..7dc874a66c5339356edebeb7ce671ab98ba88ddd --- /dev/null +++ b/router/otp_verification_router.go @@ -0,0 +1,16 @@ +package router + +import ( + "abdanhafidz.com/go-boilerplate/provider" + "github.com/gin-gonic/gin" +) + +func OTPVerificationRouter(router *gin.Engine, controller provider.ControllerProvider) { + routerGroup := router.Group("/api/v1/otp") + otpController := controller.ProvideOTPVerificationController() + + { + routerGroup.POST("/request", otpController.RequestOTP) + routerGroup.POST("/verify", otpController.VerifyOTP) + } +} diff --git a/router/payment_callback_router.go b/router/payment_callback_router.go new file mode 100644 index 0000000000000000000000000000000000000000..6358b7f66f6df2aab0d95f3de608b23e98bab001 --- /dev/null +++ b/router/payment_callback_router.go @@ -0,0 +1,13 @@ +package router + +import ( + "abdanhafidz.com/go-boilerplate/provider" + "github.com/gin-gonic/gin" +) + +func PaymentCallbackRouter(r *gin.Engine, controller provider.ControllerProvider) { + v1 := r.Group("/api/v1") + { + v1.POST("/payment/callback", controller.ProvidePaymentCallbackController().HandleCallback) + } +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000000000000000000000000000000000000..8b655a357b25ecb84134506476abe559bab11307 --- /dev/null +++ b/router/router.go @@ -0,0 +1,33 @@ +package router + +import ( + "abdanhafidz.com/go-boilerplate/provider" + "github.com/gin-gonic/gin" +) + +func RunRouter(appProvider provider.AppProvider) { + router, controller, config, middleware := appProvider.ProvideRouter(), appProvider.ProvideControllers(), appProvider.ProvideConfig(), appProvider.ProvideMiddlewares() + + router.GET("/health-check", func(ctx *gin.Context) { + ctx.JSON(200, gin.H{ + "status": "OK", + "message": "Service is up and running", + "address": config.ProvideEnvConfig().GetTCPAddress(), + }) + }) + + router.GET("/", func(ctx *gin.Context) { + ctx.JSON(200, gin.H{ + "status": "OK", + "message": "Welcome to Danzapp API", + }) + }) + + AuthenticationRouter(router, middleware, controller) + OTPVerificationRouter(router, controller) + HomeRouter(router, middleware, controller) + PaymentCallbackRouter(router, controller) + SwaggerRouter(router) + + router.Run(config.ProvideEnvConfig().GetTCPAddress()) +} diff --git a/router/swagger_router.go b/router/swagger_router.go new file mode 100644 index 0000000000000000000000000000000000000000..7ec057de5b351023d06add6dca22571dcdc5a416 --- /dev/null +++ b/router/swagger_router.go @@ -0,0 +1,12 @@ +package router + +import ( + _ "abdanhafidz.com/go-boilerplate/swagger/docs" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func SwaggerRouter(router *gin.Engine) { + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) +} diff --git a/services/home_service.go b/services/home_service.go new file mode 100644 index 0000000000000000000000000000000000000000..c493cb0973668767d4ad30f0c781e25f2129f1e0 --- /dev/null +++ b/services/home_service.go @@ -0,0 +1,39 @@ +package services + +import ( + "context" + + "abdanhafidz.com/go-boilerplate/models/dto" + "abdanhafidz.com/go-boilerplate/repositories" +) + +type HomeService interface { + GetHomeData(ctx context.Context, userId string, params map[string]interface{}) (dto.HomeResponse, error) + GetSchedules(ctx context.Context, userId string, params map[string]interface{}) (dto.ScheduleListResponse, error) + GetEventCategories(ctx context.Context) ([]dto.CategoryResponse, error) + GetClassCategories(ctx context.Context) ([]dto.CategoryResponse, error) +} + +type homeService struct { + repo repositories.HomeRepository +} + +func NewHomeService(repo repositories.HomeRepository) HomeService { + return &homeService{repo: repo} +} + +func (s *homeService) GetHomeData(ctx context.Context, userId string, params map[string]interface{}) (dto.HomeResponse, error) { + return s.repo.GetHomeData(ctx, userId, params) +} + +func (s *homeService) GetSchedules(ctx context.Context, userId string, params map[string]interface{}) (dto.ScheduleListResponse, error) { + return s.repo.GetSchedules(ctx, userId, params) +} + +func (s *homeService) GetEventCategories(ctx context.Context) ([]dto.CategoryResponse, error) { + return s.repo.GetEventCategories(ctx) +} + +func (s *homeService) GetClassCategories(ctx context.Context) ([]dto.CategoryResponse, error) { + return s.repo.GetClassCategories(ctx) +} diff --git a/services/jwt_service.go b/services/jwt_service.go new file mode 100644 index 0000000000000000000000000000000000000000..250cc8f5fbad268d78a15c31284794a94050524a --- /dev/null +++ b/services/jwt_service.go @@ -0,0 +1,86 @@ +package services + +import ( + "context" + "fmt" + + "abdanhafidz.com/go-boilerplate/models/dto" + http_error "abdanhafidz.com/go-boilerplate/models/error" + "github.com/golang-jwt/jwt/v4" + "golang.org/x/crypto/bcrypt" +) + +type JWTService interface { + GenerateToken(ctx context.Context, payload dto.JWTCustomClaims) (token string, err error) + ValidateToken(ctx context.Context, tokenStr string) (claim *dto.JWTCustomClaims, err error) + VerifyPassword(ctx context.Context, hashedPassword string, password string) error +} + +type jwtService struct { + secretKey string +} + +func NewJWTService(secretKey string) JWTService { + return &jwtService{ + secretKey: secretKey, + } +} + +func (s *jwtService) GenerateToken(ctx context.Context, payload dto.JWTCustomClaims) (token string, err error) { + + claims := jwt.MapClaims{ + "user_id": payload.UserId, + "role": payload.Role, + } + + fmt.Println(s.secretKey) + + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err_convertion := jwtToken.SignedString([]byte(s.secretKey)) + + if err_convertion != nil { + return "", http_error.INTERNAL_SERVER_ERROR + } + + return token, nil +} +func (s *jwtService) VerifyPassword(ctx context.Context, hashedPassword string, password string) error { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + if err != nil { + return http_error.WRONG_PASSWORD + } + return nil +} + +func (s *jwtService) ValidateToken(ctx context.Context, tokenStr string) (claim *dto.JWTCustomClaims, err error) { + token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return "", http_error.INTERNAL_SERVER_ERROR + } + return []byte(s.secretKey), nil + }) + + if err != nil || !token.Valid { + return nil, http_error.INVALID_TOKEN + } + + claims, ok := token.Claims.(jwt.MapClaims) + + if !ok { + return nil, http_error.INTERNAL_SERVER_ERROR + } + + user_id, ok := claims["user_id"].(string) + if !ok { + return nil, http_error.INTERNAL_SERVER_ERROR + } + role, ok := claims["role"].(string) + if !ok { + return nil, http_error.INTERNAL_SERVER_ERROR + } + + return &dto.JWTCustomClaims{ + UserId: user_id, + Role: role, + }, nil +} diff --git a/services/otp_verification_service.go b/services/otp_verification_service.go new file mode 100644 index 0000000000000000000000000000000000000000..4ced33ca1a86fef1cd796c5dc35e5f1080702489 --- /dev/null +++ b/services/otp_verification_service.go @@ -0,0 +1,46 @@ +package services + +import ( + "context" + + entity "abdanhafidz.com/go-boilerplate/models/entity" + "abdanhafidz.com/go-boilerplate/repositories" +) + +type OTPVerificationService interface { + RequestOTP(ctx context.Context, phoneNumber string) error + VerifyOTP(ctx context.Context, phoneNumber string, code string) error +} + +type otpVerificationService struct { + userService UserService + otpVerificationRepository repositories.OTPVerificationRepository +} + +func NewOTPVerificationService(userService UserService, otpVerificationRepository repositories.OTPVerificationRepository) OTPVerificationService { + return &otpVerificationService{ + userService: userService, + otpVerificationRepository: otpVerificationRepository, + } +} + +func (s *otpVerificationService) RequestOTP(ctx context.Context, phoneNumber string) error { + // Stub + return nil +} + +func (s *otpVerificationService) VerifyOTP(ctx context.Context, phoneNumber string, code string) error { + // Stub + _, err := s.otpVerificationRepository.GetByPhoneNumberAndCode(ctx, phoneNumber, code) + if err != nil { + return err + } + + user, err := s.userService.GetById(ctx, phoneNumber) // Just dummy stub + if err != nil { + user = entity.User{PhoneNumber: phoneNumber} + s.userService.Update(ctx, user) + } + + return nil +} diff --git a/services/payment_service.go b/services/payment_service.go new file mode 100644 index 0000000000000000000000000000000000000000..2e16197ab2d35212407a0140dbfa86bec3cba020 --- /dev/null +++ b/services/payment_service.go @@ -0,0 +1,41 @@ +package services + +import ( + "context" + + "github.com/google/uuid" + "github.com/xendit/xendit-go/v7" +) + +type PaymentService interface { + PaySomething(ctx context.Context, accountId uuid.UUID, amount float64) + ConfirmPayment(ctx context.Context, paymentId string) error + CancelPayment(ctx context.Context, paymentId string) error + ExpirePayment(ctx context.Context, paymentId string) error +} + +type paymentService struct { + xenditClient *xendit.APIClient +} + +func NewPaymentService(xenditClient *xendit.APIClient) PaymentService { + return &paymentService{ + xenditClient: xenditClient, + } +} + +func (s *paymentService) PaySomething(ctx context.Context, accountId uuid.UUID, amount float64) { + +} + +func (s *paymentService) ConfirmPayment(ctx context.Context, paymentId string) error { + return nil +} + +func (s *paymentService) CancelPayment(ctx context.Context, paymentId string) error { + return nil +} + +func (s *paymentService) ExpirePayment(ctx context.Context, paymentId string) error { + return nil +} diff --git a/services/service.go b/services/service.go new file mode 100644 index 0000000000000000000000000000000000000000..81e883fe0f3ace0029faf664d5c51482ef4d7f06 --- /dev/null +++ b/services/service.go @@ -0,0 +1 @@ +package services diff --git a/services/user_service.go b/services/user_service.go new file mode 100644 index 0000000000000000000000000000000000000000..76a4cc92c13aa0418948588969c7b1a07b9e982e --- /dev/null +++ b/services/user_service.go @@ -0,0 +1,119 @@ +package services + +import ( + "context" + "errors" + + "abdanhafidz.com/go-boilerplate/models/dto" + entity "abdanhafidz.com/go-boilerplate/models/entity" + "abdanhafidz.com/go-boilerplate/repositories" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +type UserService interface { + Create(ctx context.Context, name, emailStr, username, password string) (entity.User, error) + Validate(ctx context.Context, emailOrUsername, password string) (dto.AuthenticatedUser, error) + ChangePassword(ctx context.Context, id string, oldPassword, newPassword string) (dto.AuthenticatedUser, error) + GetById(ctx context.Context, id string) (entity.User, error) + Update(ctx context.Context, user entity.User) (entity.User, error) +} + +type userService struct { + jwtService JWTService + userRepository repositories.UserRepository +} + +func NewUserService(jwtService JWTService, userRepository repositories.UserRepository) UserService { + return &userService{ + jwtService: jwtService, + userRepository: userRepository, + } +} + +func (s *userService) Create(ctx context.Context, name, emailStr, username, password string) (entity.User, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return entity.User{}, err + } + hashStr := string(hashedPassword) + + email := emailStr + + user := entity.User{ + ID: uuid.NewString(), // assuming UUID string + FullName: name, + Nickname: username, + Email: &email, + PhoneNumber: uuid.NewString(), // Generate stub phone for now, it's NOT NULL in schema + PasswordHash: &hashStr, + } + + return s.userRepository.CreateUser(ctx, user) +} + +func (s *userService) Validate(ctx context.Context, emailOrUsername, password string) (dto.AuthenticatedUser, error) { + // Try email first + user, err := s.userRepository.GetUserByEmail(ctx, emailOrUsername) + if err != nil { + // Try Phone + user, err = s.userRepository.GetUserByPhoneNumber(ctx, emailOrUsername) + if err != nil { + return dto.AuthenticatedUser{}, errors.New("invalid credentials") + } + } + + if user.PasswordHash == nil || bcrypt.CompareHashAndPassword([]byte(*user.PasswordHash), []byte(password)) != nil { + return dto.AuthenticatedUser{}, errors.New("invalid credentials") + } + + token, _ := s.jwtService.GenerateToken(ctx, dto.JWTCustomClaims{ + UserId: user.ID, + Role: "", + }) // stub role + + return dto.AuthenticatedUser{ + User: user, + Token: token, + }, nil +} + +func (s *userService) ChangePassword(ctx context.Context, id string, oldPassword, newPassword string) (dto.AuthenticatedUser, error) { + user, err := s.userRepository.GetUserById(ctx, id) + if err != nil { + return dto.AuthenticatedUser{}, err + } + + if user.PasswordHash == nil || bcrypt.CompareHashAndPassword([]byte(*user.PasswordHash), []byte(oldPassword)) != nil { + return dto.AuthenticatedUser{}, errors.New("invalid password") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return dto.AuthenticatedUser{}, err + } + hashStr := string(hashedPassword) + user.PasswordHash = &hashStr + + user, err = s.userRepository.UpdateUser(ctx, user) + if err != nil { + return dto.AuthenticatedUser{}, err + } + + token, _ := s.jwtService.GenerateToken(ctx, dto.JWTCustomClaims{ + UserId: user.ID, + Role: "", + }) + return dto.AuthenticatedUser{ + User: user, + Token: token, + }, nil +} + +func (s *userService) GetById(ctx context.Context, id string) (entity.User, error) { + return s.userRepository.GetUserById(ctx, id) +} + +func (s *userService) Update(ctx context.Context, user entity.User) (entity.User, error) { + return s.userRepository.UpdateUser(ctx, user) +} diff --git a/space/.gitattributes b/space/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..a6344aac8c09253b3b630fb776ae94478aa0275b --- /dev/null +++ b/space/.gitattributes @@ -0,0 +1,35 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/swagger/README.md b/swagger/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0c1fe41d543d2b42bd665481bebc084280620b11 --- /dev/null +++ b/swagger/README.md @@ -0,0 +1,6 @@ +# Swagger Quzuu Ready + +1. Copy folder `swagger/` to your project root +2. Import `_ "your_module/swagger/docs"` in main.go +3. Run: swag init --parseDependency --parseInternal +4. Open: /swagger/index.html diff --git a/swagger/docs/docs.go b/swagger/docs/docs.go new file mode 100644 index 0000000000000000000000000000000000000000..6c713c73c05f6bd5df2b512544c20dc87fb78c16 --- /dev/null +++ b/swagger/docs/docs.go @@ -0,0 +1,719 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Abdan Hafidz", + "email": "admin@example.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/class-categories": { + "get": { + "description": "Get master list of class categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Class Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/event-categories": { + "get": { + "description": "Get master list of event categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Event Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/home": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Return all sections needed by the home page in one aggregated response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Home Page Data", + "parameters": [ + { + "type": "string", + "description": "Selected date for schedule card section (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "number", + "description": "User latitude", + "name": "latitude", + "in": "query" + }, + { + "type": "number", + "description": "User longitude", + "name": "longitude", + "in": "query" + }, + { + "type": "string", + "description": "User city", + "name": "city", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.HomeResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/payment/callback": { + "post": { + "description": "Receive and process payment status updates from Xendit", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Payment" + ], + "summary": "Handle Xendit Payment Callback", + "parameters": [ + { + "description": "Xendit Callback Payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuccessResponse-any" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/schedules": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a list of class or event schedules based on date range, city, filters, etc.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Schedule List", + "parameters": [ + { + "type": "string", + "description": "Exact date (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "string", + "description": "Start date range (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date range (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Filter by city", + "name": "city", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class level (e.g., beginner)", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class category (e.g., salsa)", + "name": "category", + "in": "query" + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Items per page (default: 10)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ScheduleListResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "dto.CategoryResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CategoryResponseWrapper": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CategoryResponse" + } + }, + "success": { + "type": "boolean" + } + } + }, + "dto.ErrorResponse": { + "type": "object", + "properties": { + "errors": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + }, + "dto.HomeBeginnerClassSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeDay": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "day_name_short": { + "type": "string" + }, + "day_number": { + "type": "integer" + }, + "has_schedule": { + "type": "boolean" + }, + "is_selected": { + "type": "boolean" + } + } + }, + "dto.HomeEventSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeHeroBanner": { + "type": "object", + "properties": { + "badge_text": { + "type": "string" + }, + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.HomeNotifications": { + "type": "object", + "properties": { + "unread_count": { + "type": "integer" + } + } + }, + "dto.HomePromoStrip": { + "type": "object", + "properties": { + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "dto.HomeResponse": { + "type": "object", + "properties": { + "beginner_class_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeBeginnerClassSection" + } + }, + "event_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeEventSection" + } + }, + "hero_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeHeroBanner" + } + }, + "notifications": { + "$ref": "#/definitions/dto.HomeNotifications" + }, + "promo_strip": { + "$ref": "#/definitions/dto.HomePromoStrip" + }, + "search": { + "$ref": "#/definitions/dto.HomeSearch" + }, + "user": { + "$ref": "#/definitions/dto.HomeUser" + }, + "week_schedule": { + "$ref": "#/definitions/dto.HomeWeekSchedule" + } + } + }, + "dto.HomeResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.HomeResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.HomeSearch": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + } + } + }, + "dto.HomeUser": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, + "dto.HomeWeekSchedule": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeDay" + } + }, + "end_date": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleItem" + } + }, + "selected_date": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "view_all_url": { + "type": "string" + }, + "week_label": { + "type": "string" + } + } + }, + "dto.PaginationMeta": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "total_items": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "dto.PriceData": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "display": { + "type": "string" + } + } + }, + "dto.ScheduleItem": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListItem": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "available_slots": { + "type": "integer" + }, + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "level": { + "type": "string" + }, + "price": { + "$ref": "#/definitions/dto.PriceData" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "thumbnail_url": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleListItem" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationMeta" + } + } + }, + "dto.ScheduleListResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ScheduleListResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.SuccessResponse-any": { + "type": "object", + "properties": { + "data": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0.0", + Host: "localhost:8000", + BasePath: "/", + Schemes: []string{"http", "https"}, + Title: "Go Boilerplate API", + Description: "Backend API documentation", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/swagger/docs/swagger.json b/swagger/docs/swagger.json new file mode 100644 index 0000000000000000000000000000000000000000..b8eb4641c0ff81dae1cad980efe931afd62c289d --- /dev/null +++ b/swagger/docs/swagger.json @@ -0,0 +1,699 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "Backend API documentation", + "title": "Go Boilerplate API", + "contact": { + "name": "Abdan Hafidz", + "email": "admin@example.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "version": "1.0.0" + }, + "host": "localhost:8000", + "basePath": "/", + "paths": { + "/api/v1/class-categories": { + "get": { + "description": "Get master list of class categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Class Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/event-categories": { + "get": { + "description": "Get master list of event categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Master Data" + ], + "summary": "Get Event Categories", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CategoryResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/home": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Return all sections needed by the home page in one aggregated response.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Home Page Data", + "parameters": [ + { + "type": "string", + "description": "Selected date for schedule card section (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "number", + "description": "User latitude", + "name": "latitude", + "in": "query" + }, + { + "type": "number", + "description": "User longitude", + "name": "longitude", + "in": "query" + }, + { + "type": "string", + "description": "User city", + "name": "city", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.HomeResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/payment/callback": { + "post": { + "description": "Receive and process payment status updates from Xendit", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Payment" + ], + "summary": "Handle Xendit Payment Callback", + "parameters": [ + { + "description": "Xendit Callback Payload", + "name": "request", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuccessResponse-any" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/api/v1/schedules": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a list of class or event schedules based on date range, city, filters, etc.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Home Dashboard" + ], + "summary": "Get Schedule List", + "parameters": [ + { + "type": "string", + "description": "Exact date (YYYY-MM-DD)", + "name": "date", + "in": "query" + }, + { + "type": "string", + "description": "Start date range (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date range (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Filter by city", + "name": "city", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class level (e.g., beginner)", + "name": "level", + "in": "query" + }, + { + "type": "string", + "description": "Filter by class category (e.g., salsa)", + "name": "category", + "in": "query" + }, + { + "type": "integer", + "description": "Page number (default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Items per page (default: 10)", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ScheduleListResponseWrapper" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "dto.CategoryResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "dto.CategoryResponseWrapper": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CategoryResponse" + } + }, + "success": { + "type": "boolean" + } + } + }, + "dto.ErrorResponse": { + "type": "object", + "properties": { + "errors": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + }, + "dto.HomeBeginnerClassSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeDay": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "day_name_short": { + "type": "string" + }, + "day_number": { + "type": "integer" + }, + "has_schedule": { + "type": "boolean" + }, + "is_selected": { + "type": "boolean" + } + } + }, + "dto.HomeEventSection": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "dto.HomeHeroBanner": { + "type": "object", + "properties": { + "badge_text": { + "type": "string" + }, + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.HomeNotifications": { + "type": "object", + "properties": { + "unread_count": { + "type": "integer" + } + } + }, + "dto.HomePromoStrip": { + "type": "object", + "properties": { + "cta_target_id": { + "type": "string" + }, + "cta_text": { + "type": "string" + }, + "cta_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "text": { + "type": "string" + } + } + }, + "dto.HomeResponse": { + "type": "object", + "properties": { + "beginner_class_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeBeginnerClassSection" + } + }, + "event_sections": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeEventSection" + } + }, + "hero_banners": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeHeroBanner" + } + }, + "notifications": { + "$ref": "#/definitions/dto.HomeNotifications" + }, + "promo_strip": { + "$ref": "#/definitions/dto.HomePromoStrip" + }, + "search": { + "$ref": "#/definitions/dto.HomeSearch" + }, + "user": { + "$ref": "#/definitions/dto.HomeUser" + }, + "week_schedule": { + "$ref": "#/definitions/dto.HomeWeekSchedule" + } + } + }, + "dto.HomeResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.HomeResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.HomeSearch": { + "type": "object", + "properties": { + "placeholder": { + "type": "string" + } + } + }, + "dto.HomeUser": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "id": { + "type": "string" + } + } + }, + "dto.HomeWeekSchedule": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.HomeDay" + } + }, + "end_date": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleItem" + } + }, + "selected_date": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "view_all_url": { + "type": "string" + }, + "week_label": { + "type": "string" + } + } + }, + "dto.PaginationMeta": { + "type": "object", + "properties": { + "limit": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "total_items": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "dto.PriceData": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "display": { + "type": "string" + } + } + }, + "dto.ScheduleItem": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListItem": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "available_slots": { + "type": "integer" + }, + "category": { + "type": "string" + }, + "city": { + "type": "string" + }, + "class_id": { + "type": "string" + }, + "date": { + "type": "string" + }, + "display_time": { + "type": "string" + }, + "end_time": { + "type": "string" + }, + "is_booked": { + "type": "boolean" + }, + "level": { + "type": "string" + }, + "price": { + "$ref": "#/definitions/dto.PriceData" + }, + "schedule_id": { + "type": "string" + }, + "start_time": { + "type": "string" + }, + "studio_name": { + "type": "string" + }, + "teacher_name": { + "type": "string" + }, + "thumbnail_url": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "dto.ScheduleListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.ScheduleListItem" + } + }, + "pagination": { + "$ref": "#/definitions/dto.PaginationMeta" + } + } + }, + "dto.ScheduleListResponseWrapper": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ScheduleListResponse" + }, + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + }, + "dto.SuccessResponse-any": { + "type": "object", + "properties": { + "data": {}, + "message": {}, + "meta_data": {}, + "status": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/swagger/docs/swagger.yaml b/swagger/docs/swagger.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a4d5e872bc5499525929dd8663b88689e5469d49 --- /dev/null +++ b/swagger/docs/swagger.yaml @@ -0,0 +1,458 @@ +basePath: / +definitions: + dto.CategoryResponse: + properties: + id: + type: string + image_url: + type: string + name: + type: string + type: object + dto.CategoryResponseWrapper: + properties: + data: + items: + $ref: '#/definitions/dto.CategoryResponse' + type: array + success: + type: boolean + type: object + dto.ErrorResponse: + properties: + errors: {} + message: {} + meta_data: {} + status: + type: string + type: object + dto.HomeBeginnerClassSection: + properties: + id: + type: string + image_url: + type: string + name: + type: string + type: + type: string + type: object + dto.HomeDay: + properties: + date: + type: string + day_name_short: + type: string + day_number: + type: integer + has_schedule: + type: boolean + is_selected: + type: boolean + type: object + dto.HomeEventSection: + properties: + id: + type: string + image_url: + type: string + name: + type: string + type: + type: string + type: object + dto.HomeHeroBanner: + properties: + badge_text: + type: string + cta_target_id: + type: string + cta_text: + type: string + cta_type: + type: string + id: + type: string + image_url: + type: string + subtitle: + type: string + title: + type: string + type: object + dto.HomeNotifications: + properties: + unread_count: + type: integer + type: object + dto.HomePromoStrip: + properties: + cta_target_id: + type: string + cta_text: + type: string + cta_type: + type: string + id: + type: string + text: + type: string + type: object + dto.HomeResponse: + properties: + beginner_class_sections: + items: + $ref: '#/definitions/dto.HomeBeginnerClassSection' + type: array + event_sections: + items: + $ref: '#/definitions/dto.HomeEventSection' + type: array + hero_banners: + items: + $ref: '#/definitions/dto.HomeHeroBanner' + type: array + notifications: + $ref: '#/definitions/dto.HomeNotifications' + promo_strip: + $ref: '#/definitions/dto.HomePromoStrip' + search: + $ref: '#/definitions/dto.HomeSearch' + user: + $ref: '#/definitions/dto.HomeUser' + week_schedule: + $ref: '#/definitions/dto.HomeWeekSchedule' + type: object + dto.HomeResponseWrapper: + properties: + data: + $ref: '#/definitions/dto.HomeResponse' + message: + type: string + success: + type: boolean + type: object + dto.HomeSearch: + properties: + placeholder: + type: string + type: object + dto.HomeUser: + properties: + avatar_url: + type: string + first_name: + type: string + full_name: + type: string + id: + type: string + type: object + dto.HomeWeekSchedule: + properties: + days: + items: + $ref: '#/definitions/dto.HomeDay' + type: array + end_date: + type: string + items: + items: + $ref: '#/definitions/dto.ScheduleItem' + type: array + selected_date: + type: string + start_date: + type: string + view_all_url: + type: string + week_label: + type: string + type: object + dto.PaginationMeta: + properties: + limit: + type: integer + page: + type: integer + total_items: + type: integer + total_pages: + type: integer + type: object + dto.PriceData: + properties: + amount: + type: integer + currency: + type: string + display: + type: string + type: object + dto.ScheduleItem: + properties: + city: + type: string + class_id: + type: string + date: + type: string + display_time: + type: string + end_time: + type: string + is_booked: + type: boolean + schedule_id: + type: string + start_time: + type: string + studio_name: + type: string + teacher_name: + type: string + title: + type: string + type: object + dto.ScheduleListItem: + properties: + address: + type: string + available_slots: + type: integer + category: + type: string + city: + type: string + class_id: + type: string + date: + type: string + display_time: + type: string + end_time: + type: string + is_booked: + type: boolean + level: + type: string + price: + $ref: '#/definitions/dto.PriceData' + schedule_id: + type: string + start_time: + type: string + studio_name: + type: string + teacher_name: + type: string + thumbnail_url: + type: string + title: + type: string + type: object + dto.ScheduleListResponse: + properties: + items: + items: + $ref: '#/definitions/dto.ScheduleListItem' + type: array + pagination: + $ref: '#/definitions/dto.PaginationMeta' + type: object + dto.ScheduleListResponseWrapper: + properties: + data: + $ref: '#/definitions/dto.ScheduleListResponse' + message: + type: string + success: + type: boolean + type: object + dto.SuccessResponse-any: + properties: + data: {} + message: {} + meta_data: {} + status: + type: string + type: object +host: localhost:8000 +info: + contact: + email: admin@example.com + name: Abdan Hafidz + description: Backend API documentation + license: + name: MIT + url: https://opensource.org/licenses/MIT + title: Go Boilerplate API + version: 1.0.0 +paths: + /api/v1/class-categories: + get: + consumes: + - application/json + description: Get master list of class categories + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CategoryResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Get Class Categories + tags: + - Master Data + /api/v1/event-categories: + get: + consumes: + - application/json + description: Get master list of event categories + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CategoryResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Get Event Categories + tags: + - Master Data + /api/v1/home: + get: + consumes: + - application/json + description: Return all sections needed by the home page in one aggregated response. + parameters: + - description: Selected date for schedule card section (YYYY-MM-DD) + in: query + name: date + type: string + - description: User latitude + in: query + name: latitude + type: number + - description: User longitude + in: query + name: longitude + type: number + - description: User city + in: query + name: city + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.HomeResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + security: + - BearerAuth: [] + summary: Get Home Page Data + tags: + - Home Dashboard + /api/v1/payment/callback: + post: + consumes: + - application/json + description: Receive and process payment status updates from Xendit + parameters: + - description: Xendit Callback Payload + in: body + name: request + required: true + schema: + additionalProperties: true + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SuccessResponse-any' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Handle Xendit Payment Callback + tags: + - Payment + /api/v1/schedules: + get: + consumes: + - application/json + description: Get a list of class or event schedules based on date range, city, + filters, etc. + parameters: + - description: Exact date (YYYY-MM-DD) + in: query + name: date + type: string + - description: Start date range (YYYY-MM-DD) + in: query + name: start_date + type: string + - description: End date range (YYYY-MM-DD) + in: query + name: end_date + type: string + - description: Filter by city + in: query + name: city + type: string + - description: Filter by class level (e.g., beginner) + in: query + name: level + type: string + - description: Filter by class category (e.g., salsa) + in: query + name: category + type: string + - description: 'Page number (default: 1)' + in: query + name: page + type: integer + - description: 'Items per page (default: 10)' + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ScheduleListResponseWrapper' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + security: + - BearerAuth: [] + summary: Get Schedule List + tags: + - Home Dashboard +schemes: +- http +- https +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/swagger/swagger_info.go b/swagger/swagger_info.go new file mode 100644 index 0000000000000000000000000000000000000000..efdc24ac92fee6dbae0899e3211811f70ca1e261 --- /dev/null +++ b/swagger/swagger_info.go @@ -0,0 +1,27 @@ +package swagger + +// ===================== +// Swagger Global Config +// ===================== + +// @title Go Boilerplate API +// @version 1.0.0 +// @description Backend API documentation + +// @contact.name Abdan Hafidz +// @contact.email admin@example.com + +// @license.name MIT +// @license.url https://opensource.org/licenses/MIT + +// @host localhost:8000 +// @BasePath / +// @schemes http https + +// ===================== +// Security +// ===================== + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization diff --git a/tmp_build_errors.txt b/tmp_build_errors.txt new file mode 100644 index 0000000000000000000000000000000000000000..b096d1bf258769c8d3966f9036c0643756952b1c Binary files /dev/null and b/tmp_build_errors.txt differ diff --git a/utils/logger_util.go b/utils/logger_util.go new file mode 100644 index 0000000000000000000000000000000000000000..7034609edde3a87fd9e0ea16efa6d0ab5c059fd1 --- /dev/null +++ b/utils/logger_util.go @@ -0,0 +1,31 @@ +package utils + +import ( + "fmt" + "log" + "os" +) + +func InternalErrorLog(err_log error) { + fmt.Println("There is an error!") + + file, err := os.OpenFile("logs/error_log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + + if err != nil { + log.Fatal(err) + } + log.Println("Error Log :", err_log) + log.SetOutput(file) +} + +func SecurityLog(security_log string) { + fmt.Println("There is an error!") + + file, err := os.OpenFile("logs/security_log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + + if err != nil { + log.Fatal(err) + } + log.Println("Security Log :", security_log) + log.SetOutput(file) +} \ No newline at end of file diff --git a/utils/response_util.go b/utils/response_util.go new file mode 100644 index 0000000000000000000000000000000000000000..f22f08cb90e1e1cc0604e887b9eea4a7cabdcd97 --- /dev/null +++ b/utils/response_util.go @@ -0,0 +1,112 @@ +package utils + +import ( + "errors" + + "abdanhafidz.com/go-boilerplate/models/dto" + http_error "abdanhafidz.com/go-boilerplate/models/error" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func ResponseOK[Tdata any, TMetaData any](c *gin.Context, metaData TMetaData, data Tdata) { + c.JSON(200, dto.SuccessResponse[Tdata]{ + Status: "success", + Data: data, + Message: "Data retrieved Successfully!", + MetaData: metaData, + }) +} + +func ResponseFAILED[TMetaData any](c *gin.Context, metaData TMetaData, err error) { + if errors.Is(err, http_error.BAD_REQUEST_ERROR) { + c.JSON(400, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: "Invalid request format!", + MetaData: metaData, + }) + return + } else if errors.Is(err, http_error.INTERNAL_SERVER_ERROR) { + c.JSON(500, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: "Internal Server Error!", + MetaData: metaData, + }) + return + } else if errors.Is(err, http_error.UNAUTHORIZED) { + c.JSON(401, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: "Unauthorized, you don't have permission to access this service!", + MetaData: metaData, + }) + return + } else if errors.Is(err, http_error.PAYMENT_REQUIRED) { + c.JSON(402, dto.SuccessResponse[TMetaData]{ + Status: "action_required", + Data: metaData, + Message: http_error.PAYMENT_REQUIRED.Error(), + }) + return + } else if errors.Is(err, http_error.NOT_FOUND_ERROR) || errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(404, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: "There is not data with given credential / given parameter!", + MetaData: metaData, + }) + return + } else if errors.Is(err, http_error.TIMEOUT) { + c.JSON(504, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: "Server took to long to respond!", + MetaData: metaData, + }) + return + } else if errors.Is(err, http_error.FORBIDDEN_ERROR) || errors.Is(err, http_error.INVALID_CODE) { + c.JSON(403, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: err.Error(), + MetaData: metaData, + }) + return + } else if errors.Is(err, http_error.EVENT_START_DATE_IN_PAST) || + errors.Is(err, http_error.EVENT_START_DATE_INVALID) || + errors.Is(err, http_error.EVENT_END_DATE_INVALID) || + errors.Is(err, http_error.INVALID_DATE_FORMAT) { + c.JSON(400, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: err.Error(), + MetaData: metaData, + }) + return + } else { + c.JSON(405, dto.ErrorResponse{ + Status: "error", + Error: err, + Message: err.Error(), + MetaData: metaData, + }) + return + } +} + +func SendResponse[Tdata any, TMetaData any](c *gin.Context, metaData TMetaData, data Tdata, err error) { + if !c.IsAborted() { + if err != nil { + ResponseFAILED(c, metaData, err) + c.Abort() + return + } else { + ResponseOK(c, metaData, data) + c.Abort() + return + } + } + +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..e97313943ac391275e242de73fe48f7756abac7e --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,75 @@ +package utils + +import ( + "os" + "regexp" + "strings" + "time" + + http_error "abdanhafidz.com/go-boilerplate/models/error" + "github.com/google/uuid" +) + +func ToUUID(s any) (uuid.UUID, error) { + sStr, ok := s.(string) + if !ok { + return uuid.UUID{}, http_error.INTERNAL_SERVER_ERROR + } + + res, err := uuid.Parse(sStr) + if err != nil { + return uuid.UUID{}, http_error.INTERNAL_SERVER_ERROR + } + + return res, nil +} +func CalculateRemainingTime(startTime, dueTime time.Time) int { + now := time.Now() + if startTime.After(now) { + return int(dueTime.Sub(startTime).Seconds()) + } + remaining := int(dueTime.Sub(now).Seconds()) + if remaining < 0 { + return 0 + } + return remaining / 60 +} + +func Ptr[T any](v T) *T { + return &v +} + +func TimePtrToString(t *time.Time) *string { + if t == nil { + return nil + } + s := t.Format(time.RFC3339) + return &s +} + +func ValidateCode(code string) error { + var CodeRegex = regexp.MustCompile(`^[a-zA-Z0-9]{6,12}$`) + if !CodeRegex.MatchString(code) { + return http_error.INVALID_CODE + } + return nil +} + +func GetEnv(key string) string { + // 1. Normal env + if val := os.Getenv(key); val != "" { + return val + } + + // 2. File-based secret + if file := os.Getenv(key + "_FILE"); file != "" { + data, err := os.ReadFile(file) + if err == nil { + return strings.TrimSpace(string(data)) + } else { + panic(err) + } + } + + return "" +}