feat: Revamp logs page with new HTML structure and styles

- Updated logs.html to include a modern design with Tailwind CSS.
- Added a login section for admin access to logs.
- Implemented JavaScript functions for login, log retrieval, and month selection.
- Enhanced user experience with loading indicators and error handling.

feat: Add ServeLogsPage method to LogsController

- Introduced ServeLogsPage method to serve the logs HTML page.
- Configured to respond with HTML when the Accept header is text/html.

refactor: Update logs routes to serve HTML page

- Modified routes to serve the logs HTML page without authentication.
- Adjusted protected routes for log retrieval to use API versioning.

docs: Add API documentation for user and role controllers

- Added Swagger documentation comments for user and role controller methods.
- Documented endpoints for user registration, login, and email verification.
This commit is contained in:
Habib Fatkhul Rohman 2025-10-23 00:17:07 +07:00
parent 80825a70af
commit 2546a094a3
13 changed files with 2921 additions and 89 deletions

152
README.md
View File

@ -1,7 +1,9 @@
# Golang Gin Clean Starter
You can join in the development (Open Source). **Let's Go!!!**
## Introduction 👋
> This project implements **Clean Architecture** principles with the ControllerServiceRepository pattern. This approach emphasizes clear separation of responsibilities across different layers in Golang applications. The architecture helps keep the codebase clean, testable, and scalable by dividing application logic into distinct modules with well-defined boundaries.
![image](https://github.com/user-attachments/assets/0b011bcc-f9c6-466e-a9da-964cce47a8bc)
@ -11,14 +13,18 @@ You can join in the development (Open Source). **Let's Go!!!**
The application includes a built-in logging system that allows you to monitor and track system queries. You can access the logs through a modern, user-friendly interface.
### Accessing Logs
To view the logs:
1. Make sure the application is running
2. Open your browser and navigate to:
```bash
http://your-domain/logs
```
### Features
- **Monthly Filtering**: Filter logs by selecting different months
- **Real-time Refresh**: Instantly refresh logs with the refresh button
- **Expandable Entries**: Click on any log entry to view its full content
@ -26,29 +32,37 @@ http://your-domain/logs
![Logs Interface](https://github.com/user-attachments/assets/adda0afb-a1e4-4e05-b44e-87225fe63309)
## Prerequisite 🏆
- Go Version `>= go 1.20`
- PostgreSQL Version `>= version 15.0`
## How To Use
1. Clone the repository or **Use This Template**
```bash
git clone https://github.com/Caknoooo/go-gin-clean-starter.git
```
```bash
git clone https://github.com/Caknoooo/go-gin-clean-starter.git
```
2. Navigate to the project directory:
```bash
cd go-gin-clean-starter
```
```bash
cd go-gin-clean-starter
```
3. Copy the example environment file and configure it:
```bash
cp .env.example .env
```
```bash
cp .env.example .env
```
## Available Make Commands 🚀
The project includes a comprehensive Makefile with the following commands:
### Development Commands
```bash
make dep # Install and tidy dependencies
make run # Run the application locally
@ -57,13 +71,15 @@ make test # Run tests
```
### Local Database Commands (without Docker)
```bash
make migrate-local # Run migrations locally
make seed-local # Run seeders locally
make seed-local # Run seeders locally
make migrate-seed-local # Run migrations + seeders locally
```
### Docker Commands
```bash
make init-docker # Initialize and build Docker containers
make up # Start Docker containers
@ -72,6 +88,7 @@ make logs # View Docker logs
```
### Docker Database Commands
```bash
make migrate # Run migrations in Docker
make seed # Run seeders in Docker
@ -81,89 +98,114 @@ make container-postgres # Access PostgreSQL container
```
There are 2 ways to run the application:
### With Docker
1. Build and start Docker containers:
```bash
make init-docker
```
```bash
make init-docker
```
2. Run Initial UUID V4 for Auto Generate UUID:
```bash
make init-uuid
```
```bash
make init-uuid
```
3. Run Migration and Seeder:
```bash
make migrate-seed
```
```bash
make migrate-seed
```
### Without Docker
1. Configure `.env` with your PostgreSQL credentials:
```bash
DB_HOST=localhost
DB_USER=postgres
DB_PASS=
DB_NAME=
DB_PORT=5432
```
```bash
DB_HOST=localhost
DB_USER=postgres
DB_PASS=
DB_NAME=
DB_PORT=5432
```
2. Open the terminal and set up PostgreSQL:
- If you haven't downloaded PostgreSQL, download it first.
- Run:
```bash
psql -U postgres
```
- Create the database according to what you put in `.env`:
```bash
CREATE DATABASE your_database;
\c your_database
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
\q
```
3. Install dependencies and run the application:
- If you haven't downloaded PostgreSQL, download it first.
- Run:
```bash
make dep # Install dependencies
make migrate-local # Run migrations
make seed-local # Run seeders (optional)
make run # Start the application
psql -U postgres
```
- Create the database according to what you put in `.env`:
```bash
CREATE DATABASE your_database;
\c your_database
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
\q
```
3. Install dependencies and run the application:
```bash
make dep # Install dependencies
make migrate-local # Run migrations
make seed-local # Run seeders (optional)
make run # Start the application
```
## Run Migrations, Seeder, and Script
To run migrations, seed the database, and execute a script while keeping the application running, use the following command:
```bash
go run cmd/main.go --migrate --seed --run --script:example_script
```
- ``--migrate`` will apply all pending migrations.
- ``--seed`` will seed the database with initial data.
- ``--script:example_script`` will run the specified script (replace ``example_script`` with your script name).
- ``--run`` will ensure the application continues running after executing the commands above.
- `--migrate` will apply all pending migrations.
- `--seed` will seed the database with initial data.
- `--script:example_script` will run the specified script (replace `example_script` with your script name).
- `--run` will ensure the application continues running after executing the commands above.
#### Migrate Database
To migrate the database schema
#### Migrate Database
To migrate the database schema
```bash
go run cmd/main.go --migrate
```
This command will apply all pending migrations to your PostgreSQL database specified in `.env`
#### Seeder Database
#### Seeder Database
To seed the database with initial data:
```bash
go run cmd/main.go --seed
```
This command will populate the database with initial data using the seeders defined in your application.
#### Script Run
To run a specific script:
```bash
go run cmd/main.go --script:example_script
```
Replace ``example_script`` with the actual script name in **script.go** at script folder
If you need the application to continue running after performing migrations, seeding, or executing a script, always append the ``--run`` option.
Replace `example_script` with the actual script name in **script.go** at script folder
If you need the application to continue running after performing migrations, seeding, or executing a script, always append the `--run` option.
## What did you get?
By using this template, you get a ready-to-go architecture with pre-configured endpoints. The template provides a structured foundation for building your application using Golang with Clean Architecture principles.
### Postman Documentation
You can explore the available endpoints and their usage in the [Postman Documentation](https://documenter.getpostman.com/view/29665461/2s9YJaZQCG). This documentation provides a comprehensive overview of the API endpoints, including request and response examples, making it easier to understand how to interact with the API.
### Issue / Pull Request Template
@ -171,4 +213,6 @@ You can explore the available endpoints and their usage in the [Postman Document
The repository includes templates for issues and pull requests to standardize contributions and improve the quality of discussions and code reviews.
- **Issue Template**: Helps in reporting bugs or suggesting features by providing a structured format to capture all necessary information.
- **Pull Request Template**: Guides contributors to provide a clear description of changes, related issues, and testing steps, ensuring smooth and efficient code reviews.
- **Pull Request Template**: Guides contributors to provide a clear description of changes, related issues, and testing steps, ensuring smooth and efficient code reviews.
swag init -g ./cmd/main.go -o ./docs

View File

@ -5,9 +5,12 @@ import (
"log"
"os"
"github.com/Caknoooo/go-gin-clean-starter/docs"
"github.com/Caknoooo/go-gin-clean-starter/middlewares"
"github.com/Caknoooo/go-gin-clean-starter/modules/auth"
"github.com/Caknoooo/go-gin-clean-starter/modules/role"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
// "github.com/Caknoooo/go-gin-clean-starter/modules/role"
@ -53,12 +56,40 @@ func run(server *gin.Engine) {
}
}
// @title WMS Wareify API Docs
// @version 1.0
// @description This is a sample server celler server.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8888
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Bearer token for authentication
// @tokenUrl http://localhost:3000/auth/login
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func main() {
var (
injector = do.New()
)
providers.RegisterDependencies(injector)
// set Swagger info (opsional)
docs.SwaggerInfo.Title = "WMS Wareify API"
docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.Host = "localhost:8888"
docs.SwaggerInfo.BasePath = "/api/v1"
if !args(injector) {
return
@ -88,5 +119,8 @@ func main() {
role.RegisterRoutes(server, injector)
logs.RegisterRoutes(server, injector)
// register swagger route
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
run(server)
}

View File

@ -9,10 +9,11 @@ server {
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header Authorization $http_authorization;
}
# Tambahkan route khusus untuk /logs
location /logs {
# Tambahkan route khusus untuk /admin/logs
location /admin/logs {
proxy_pass http://app:8888;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;

711
docs/docs.go Normal file
View File

@ -0,0 +1,711 @@
// 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}}",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "http://www.swagger.io/support",
"email": "support@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/auth/login": {
"post": {
"description": "Authenticate user and return access \u0026 refresh tokens.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "User login",
"parameters": [
{
"description": "Login payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UserLoginRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/auth/refresh": {
"post": {
"description": "Exchange a refresh token for a new access token (and optionally a new refresh token).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Refresh auth token",
"parameters": [
{
"description": "Refresh token payload",
"name": "request",
"in": "body",
"required": true,
"schema": {
"type": "object"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/example/helloworld": {
"get": {
"description": "do ping",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"example"
],
"summary": "ping example",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/users": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get paginated list of users for the current client. Supports filtering by name and including related roles.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get list of users",
"parameters": [
{
"type": "string",
"description": "Filter by name (partial match)",
"name": "name",
"in": "query"
},
{
"type": "integer",
"description": "Page number (default: 1)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Page size (default: 10)",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "Sort expression, e.g. created_at desc",
"name": "sort",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.UserResponse"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Update profile of the authenticated user. Use multipart/form-data if uploading files (photo).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update current user",
"parameters": [
{
"description": "Update payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UserUpdateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Delete the authenticated user's account (soft/hard depends on implementation).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Delete current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/me": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Return the authenticated user's profile.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/register": {
"post": {
"description": "Create a new user under the authenticated client. Validates input and returns created user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Register a new user",
"parameters": [
{
"description": "Register payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UserCreateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/send-verification-email": {
"post": {
"description": "Send email with verification code/link to a user's email address.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Send verification email",
"parameters": [
{
"description": "Email request payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SendVerificationEmailRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/verify-email": {
"post": {
"description": "Verify a user's email using code or token sent via email.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Verify user email",
"parameters": [
{
"description": "Verify email payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.VerifyEmailRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/{id}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get details of a user by their ID. Requires appropriate permissions.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get user by ID",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"definitions": {
"dto.SendVerificationEmailRequest": {
"type": "object",
"required": [
"email"
],
"properties": {
"email": {
"type": "string"
}
}
},
"dto.UserCreateRequest": {
"type": "object",
"required": [
"client_id",
"email",
"name",
"password",
"username"
],
"properties": {
"address": {
"type": "string"
},
"client_id": {
"type": "string"
},
"email": {
"type": "string"
},
"gender": {
"type": "string",
"maxLength": 10
},
"location_id": {
"type": "string"
},
"maintenance_group_user_id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 2
},
"password": {
"type": "string",
"minLength": 8
},
"phone": {
"type": "string",
"maxLength": 20,
"minLength": 8
},
"photo_url": {
"type": "string"
},
"username": {
"type": "string",
"maxLength": 100,
"minLength": 2
}
}
},
"dto.UserLoginRequest": {
"type": "object",
"required": [
"email",
"password"
],
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"dto.UserResponse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"email": {
"type": "string"
},
"gender": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"photo_url": {
"type": "string"
},
"roles": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.UserRolesResponse"
}
},
"username": {
"type": "string"
}
}
},
"dto.UserRolesResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"dto.UserUpdateRequest": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"client_id": {
"type": "string"
},
"email": {
"type": "string"
},
"gender": {
"type": "string",
"maxLength": 10
},
"location_id": {
"type": "string"
},
"maintenance_group_user_id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 2
},
"password": {
"type": "string",
"minLength": 8
},
"phone": {
"type": "string",
"maxLength": 20,
"minLength": 8
},
"photo_url": {
"type": "string"
},
"username": {
"type": "string",
"maxLength": 100,
"minLength": 2
}
}
},
"dto.VerifyEmailRequest": {
"type": "object",
"required": [
"token"
],
"properties": {
"token": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "Bearer token for authentication",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"externalDocs": {
"description": "OpenAPI",
"url": "https://swagger.io/resources/open-api/"
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:8888",
BasePath: "/api/v1",
Schemes: []string{},
Title: "WMS Wareify API Docs",
Description: "This is a sample server celler server.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

687
docs/swagger.json Normal file
View File

@ -0,0 +1,687 @@
{
"swagger": "2.0",
"info": {
"description": "This is a sample server celler server.",
"title": "WMS Wareify API Docs",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "http://www.swagger.io/support",
"email": "support@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0"
},
"host": "localhost:8888",
"basePath": "/api/v1",
"paths": {
"/auth/login": {
"post": {
"description": "Authenticate user and return access \u0026 refresh tokens.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "User login",
"parameters": [
{
"description": "Login payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UserLoginRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/auth/refresh": {
"post": {
"description": "Exchange a refresh token for a new access token (and optionally a new refresh token).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Refresh auth token",
"parameters": [
{
"description": "Refresh token payload",
"name": "request",
"in": "body",
"required": true,
"schema": {
"type": "object"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/example/helloworld": {
"get": {
"description": "do ping",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"example"
],
"summary": "ping example",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/users": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get paginated list of users for the current client. Supports filtering by name and including related roles.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get list of users",
"parameters": [
{
"type": "string",
"description": "Filter by name (partial match)",
"name": "name",
"in": "query"
},
{
"type": "integer",
"description": "Page number (default: 1)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Page size (default: 10)",
"name": "page_size",
"in": "query"
},
{
"type": "string",
"description": "Sort expression, e.g. created_at desc",
"name": "sort",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.UserResponse"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Update profile of the authenticated user. Use multipart/form-data if uploading files (photo).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Update current user",
"parameters": [
{
"description": "Update payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UserUpdateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"delete": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Delete the authenticated user's account (soft/hard depends on implementation).",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Delete current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/me": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Return the authenticated user's profile.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get current user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/register": {
"post": {
"description": "Create a new user under the authenticated client. Validates input and returns created user.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Register a new user",
"parameters": [
{
"description": "Register payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UserCreateRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/send-verification-email": {
"post": {
"description": "Send email with verification code/link to a user's email address.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Send verification email",
"parameters": [
{
"description": "Email request payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SendVerificationEmailRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/verify-email": {
"post": {
"description": "Verify a user's email using code or token sent via email.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Verify user email",
"parameters": [
{
"description": "Verify email payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.VerifyEmailRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/users/{id}": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get details of a user by their ID. Requires appropriate permissions.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get user by ID",
"parameters": [
{
"type": "string",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
},
"definitions": {
"dto.SendVerificationEmailRequest": {
"type": "object",
"required": [
"email"
],
"properties": {
"email": {
"type": "string"
}
}
},
"dto.UserCreateRequest": {
"type": "object",
"required": [
"client_id",
"email",
"name",
"password",
"username"
],
"properties": {
"address": {
"type": "string"
},
"client_id": {
"type": "string"
},
"email": {
"type": "string"
},
"gender": {
"type": "string",
"maxLength": 10
},
"location_id": {
"type": "string"
},
"maintenance_group_user_id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 2
},
"password": {
"type": "string",
"minLength": 8
},
"phone": {
"type": "string",
"maxLength": 20,
"minLength": 8
},
"photo_url": {
"type": "string"
},
"username": {
"type": "string",
"maxLength": 100,
"minLength": 2
}
}
},
"dto.UserLoginRequest": {
"type": "object",
"required": [
"email",
"password"
],
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"dto.UserResponse": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"email": {
"type": "string"
},
"gender": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"password": {
"type": "string"
},
"phone": {
"type": "string"
},
"photo_url": {
"type": "string"
},
"roles": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.UserRolesResponse"
}
},
"username": {
"type": "string"
}
}
},
"dto.UserRolesResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"dto.UserUpdateRequest": {
"type": "object",
"properties": {
"address": {
"type": "string"
},
"client_id": {
"type": "string"
},
"email": {
"type": "string"
},
"gender": {
"type": "string",
"maxLength": 10
},
"location_id": {
"type": "string"
},
"maintenance_group_user_id": {
"type": "string"
},
"name": {
"type": "string",
"maxLength": 100,
"minLength": 2
},
"password": {
"type": "string",
"minLength": 8
},
"phone": {
"type": "string",
"maxLength": 20,
"minLength": 8
},
"photo_url": {
"type": "string"
},
"username": {
"type": "string",
"maxLength": 100,
"minLength": 2
}
}
},
"dto.VerifyEmailRequest": {
"type": "object",
"required": [
"token"
],
"properties": {
"token": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "Bearer token for authentication",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"externalDocs": {
"description": "OpenAPI",
"url": "https://swagger.io/resources/open-api/"
}
}

462
docs/swagger.yaml Normal file
View File

@ -0,0 +1,462 @@
basePath: /api/v1
definitions:
dto.SendVerificationEmailRequest:
properties:
email:
type: string
required:
- email
type: object
dto.UserCreateRequest:
properties:
address:
type: string
client_id:
type: string
email:
type: string
gender:
maxLength: 10
type: string
location_id:
type: string
maintenance_group_user_id:
type: string
name:
maxLength: 100
minLength: 2
type: string
password:
minLength: 8
type: string
phone:
maxLength: 20
minLength: 8
type: string
photo_url:
type: string
username:
maxLength: 100
minLength: 2
type: string
required:
- client_id
- email
- name
- password
- username
type: object
dto.UserLoginRequest:
properties:
email:
type: string
password:
type: string
required:
- email
- password
type: object
dto.UserResponse:
properties:
address:
type: string
email:
type: string
gender:
type: string
id:
type: string
name:
type: string
password:
type: string
phone:
type: string
photo_url:
type: string
roles:
items:
$ref: '#/definitions/dto.UserRolesResponse'
type: array
username:
type: string
type: object
dto.UserRolesResponse:
properties:
id:
type: string
name:
type: string
type: object
dto.UserUpdateRequest:
properties:
address:
type: string
client_id:
type: string
email:
type: string
gender:
maxLength: 10
type: string
location_id:
type: string
maintenance_group_user_id:
type: string
name:
maxLength: 100
minLength: 2
type: string
password:
minLength: 8
type: string
phone:
maxLength: 20
minLength: 8
type: string
photo_url:
type: string
username:
maxLength: 100
minLength: 2
type: string
type: object
dto.VerifyEmailRequest:
properties:
token:
type: string
required:
- token
type: object
externalDocs:
description: OpenAPI
url: https://swagger.io/resources/open-api/
host: localhost:8888
info:
contact:
email: support@swagger.io
name: API Support
url: http://www.swagger.io/support
description: This is a sample server celler server.
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: http://swagger.io/terms/
title: WMS Wareify API Docs
version: "1.0"
paths:
/auth/login:
post:
consumes:
- application/json
description: Authenticate user and return access & refresh tokens.
parameters:
- description: Login payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.UserLoginRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
summary: User login
tags:
- Auth
/auth/refresh:
post:
consumes:
- application/json
description: Exchange a refresh token for a new access token (and optionally
a new refresh token).
parameters:
- description: Refresh token payload
in: body
name: request
required: true
schema:
type: object
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
"401":
description: Unauthorized
schema:
additionalProperties: true
type: object
summary: Refresh auth token
tags:
- Auth
/example/helloworld:
get:
consumes:
- application/json
description: do ping
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: ping example
tags:
- example
/users:
delete:
consumes:
- application/json
description: Delete the authenticated user's account (soft/hard depends on implementation).
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
security:
- ApiKeyAuth: []
summary: Delete current user
tags:
- Users
get:
consumes:
- application/json
description: Get paginated list of users for the current client. Supports filtering
by name and including related roles.
parameters:
- description: Filter by name (partial match)
in: query
name: name
type: string
- description: 'Page number (default: 1)'
in: query
name: page
type: integer
- description: 'Page size (default: 10)'
in: query
name: page_size
type: integer
- description: Sort expression, e.g. created_at desc
in: query
name: sort
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/dto.UserResponse'
type: array
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
security:
- ApiKeyAuth: []
summary: Get list of users
tags:
- Users
put:
consumes:
- application/json
description: Update profile of the authenticated user. Use multipart/form-data
if uploading files (photo).
parameters:
- description: Update payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.UserUpdateRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.UserResponse'
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
security:
- ApiKeyAuth: []
summary: Update current user
tags:
- Users
/users/{id}:
get:
consumes:
- application/json
description: Get details of a user by their ID. Requires appropriate permissions.
parameters:
- description: User ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.UserResponse'
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
security:
- ApiKeyAuth: []
summary: Get user by ID
tags:
- Users
/users/me:
get:
consumes:
- application/json
description: Return the authenticated user's profile.
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.UserResponse'
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
security:
- ApiKeyAuth: []
summary: Get current user
tags:
- Users
/users/register:
post:
consumes:
- application/json
description: Create a new user under the authenticated client. Validates input
and returns created user.
parameters:
- description: Register payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.UserCreateRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.UserResponse'
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties: true
type: object
summary: Register a new user
tags:
- Users
/users/send-verification-email:
post:
consumes:
- application/json
description: Send email with verification code/link to a user's email address.
parameters:
- description: Email request payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.SendVerificationEmailRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
additionalProperties: true
type: object
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
summary: Send verification email
tags:
- Users
/users/verify-email:
post:
consumes:
- application/json
description: Verify a user's email using code or token sent via email.
parameters:
- description: Verify email payload
in: body
name: body
required: true
schema:
$ref: '#/definitions/dto.VerifyEmailRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.UserResponse'
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
summary: Verify user email
tags:
- Users
securityDefinitions:
BearerAuth:
description: Bearer token for authentication
in: header
name: Authorization
type: apiKey
swagger: "2.0"

66
go.mod
View File

@ -1,52 +1,72 @@
module github.com/Caknoooo/go-gin-clean-starter
go 1.23.0
go 1.24.0
toolchain go1.24.1
require (
github.com/Caknoooo/go-pagination v0.1.0
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
github.com/gin-gonic/gin v1.10.0
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/samber/do v1.6.0
github.com/spf13/viper v1.20.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.36.0
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.43.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12
)
require (
github.com/bytedance/sonic v1.13.1 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.2.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-openapi/swag v0.25.1 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
github.com/go-openapi/swag/loading v0.25.1 // indirect
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // 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.25.0 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // 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.2 // 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/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.1 // 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.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.8.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
@ -55,15 +75,23 @@ require (
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/swaggo/gin-swagger v1.6.1 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

120
go.sum
View File

@ -1,12 +1,26 @@
github.com/Caknoooo/go-pagination v0.1.0 h1:DoSs9IaNmzOMb7I8zZddZeqyU/6Ss27lrv1G3N8b3KA=
github.com/Caknoooo/go-pagination v0.1.0/go.mod h1:JFrym1XOpBuX5ovwsJ885n6onqIVWMZwOmh1W3P2wbk=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
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.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
@ -19,10 +33,38 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
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.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
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.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8=
github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
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=
@ -31,14 +73,19 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
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.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
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=
@ -56,11 +103,15 @@ 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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -68,6 +119,8 @@ 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/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
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=
@ -79,8 +132,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@ -113,30 +172,89 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
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.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
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.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.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-20220715151400-c0bba94af5f8/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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -144,6 +262,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=

604
logs.html
View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!-- <!DOCTYPE html>
<html lang="en">
<head>
@ -197,4 +197,606 @@
</script>
</body>
</html> -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Logs - Admin</title>
<script src="https://cdn.tailwindcss.com"></script>
<script
src="https://kit.fontawesome.com/a82697a287.js"
crossorigin="anonymous"
></script>
<style>
body {
background: linear-gradient(135deg, #f6f8fc 0%, #e9ecef 100%);
min-height: 100vh;
}
.glass-effect {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.log-item {
position: relative;
max-height: 10rem;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid rgba(226, 232, 240, 0.7);
}
.log-text {
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: #4a5568;
}
.log-item:hover {
max-height: none;
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.log-item:hover .log-text {
white-space: normal;
}
.custom-select {
background: rgba(255, 255, 255, 0.9);
color: #4f46e5;
border: 2px solid rgba(79, 70, 229, 0.2);
font-weight: 600;
padding-right: 2.5rem;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%234f46e5'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 1.25rem;
}
.custom-select:hover {
border-color: #4f46e5;
background-color: rgba(79, 70, 229, 0.05);
transform: translateY(-1px);
}
.custom-select:focus {
outline: none;
border-color: #4f46e5;
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1);
}
.refresh-button {
background: rgba(255, 255, 255, 0.9);
color: #4f46e5;
width: 48px;
height: 48px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.4s ease;
border: 2px solid rgba(79, 70, 229, 0.2);
font-size: 1.25rem;
position: relative;
overflow: hidden;
}
.refresh-button::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
45deg,
rgba(79, 70, 229, 0.1) 0%,
rgba(79, 70, 229, 0) 100%
);
opacity: 0;
transition: opacity 0.3s ease;
}
.refresh-button:hover {
border-color: #4f46e5;
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(79, 70, 229, 0.15);
}
.refresh-button:hover::before {
opacity: 1;
}
.refresh-button:hover i {
transform: rotate(180deg);
}
.refresh-button i {
transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.controls-wrapper {
display: flex;
gap: 1rem;
align-items: center;
background: rgba(255, 255, 255, 0.5);
padding: 0.5rem;
border-radius: 20px;
backdrop-filter: blur(8px);
}
.login-container {
max-width: 400px;
margin: 100px auto;
padding: 2rem;
}
.btn-primary {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: white;
padding: 12px 24px;
border-radius: 12px;
border: none;
font-weight: 600;
transition: all 0.3s ease;
width: 100%;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.3);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.input-field {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 12px;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.9);
}
.input-field:focus {
outline: none;
border-color: #4f46e5;
box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1);
}
.hidden {
display: none;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
.error-message {
background: #fee2e2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 12px;
border-radius: 8px;
margin-top: 1rem;
}
</style>
</head>
<body class="font-sans">
<!-- Login Section -->
<div
id="loginSection"
class="max-w-md mx-auto p-8 glass-effect rounded-3xl shadow-2xl mt-12 login-container"
>
<div class="text-center mb-8">
<h1
class="text-3xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent mb-2"
>
Admin Logs
</h1>
<p class="text-gray-500">Login to access system logs</p>
</div>
<form id="loginForm" onsubmit="login(event)">
<div class="space-y-4">
<div>
<label
for="email"
class="block text-sm font-medium text-gray-700 mb-2"
>Email</label
>
<input
type="email"
id="email"
required
placeholder="admin@example.com"
class="input-field"
/>
</div>
<div>
<label
for="password"
class="block text-sm font-medium text-gray-700 mb-2"
>Password</label
>
<input
type="password"
id="password"
required
placeholder="Enter your password"
class="input-field"
/>
</div>
</div>
<button type="submit" id="loginBtn" class="btn-primary mt-6">
<span id="loginText">Login</span>
<span id="loginLoading" class="hidden">
<i class="fa-solid fa-spinner fa-spin"></i> Logging in...
</span>
</button>
</form>
<div id="errorMessage" class="error-message hidden"></div>
</div>
<!-- Logs Section (Hidden until login) -->
<div id="logsSection" class="hidden">
<div
class="max-w-5xl mx-auto p-8 glass-effect rounded-3xl shadow-2xl mt-12 mb-12"
>
<div class="flex items-center justify-between mb-12">
<div class="flex items-center gap-4">
<div>
<h1
class="text-4xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent"
>
Query Logs
</h1>
<p class="text-gray-500 mt-2">Monitor and track system queries</p>
</div>
<!-- <span
class="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm font-medium"
>
<i class="fa-solid fa-circle-check mr-1"></i> Admin
</span> -->
</div>
<div class="flex items-center gap-4">
<div class="controls-wrapper">
<button
onclick="refreshLogs()"
class="refresh-button"
title="Refresh logs"
>
<i class="fa-solid fa-arrows-rotate"></i>
</button>
<div class="relative">
<select
id="month"
onchange="changeMonth()"
class="custom-select w-48 px-6 py-3 rounded-xl text-sm font-medium"
>
<option value="january">January</option>
<option value="february">February</option>
<option value="march">March</option>
<option value="april">April</option>
<option value="may">May</option>
<option value="june">June</option>
<option value="july">July</option>
<option value="august">August</option>
<option value="september">September</option>
<option value="october">October</option>
<option value="november">November</option>
<option value="december">December</option>
</select>
</div>
</div>
<button
onclick="logout()"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-xl transition-all flex items-center gap-2"
>
<i class="fa-solid fa-right-from-bracket"></i>
Logout
</button>
</div>
</div>
<div id="logsContent">
<!-- Logs will be loaded here -->
</div>
</div>
</div>
<script>
const API_BASE = "/api/v1/auth";
const LOGS_BASE = "/api/v1/logs";
// Check if already logged in on page load
document.addEventListener("DOMContentLoaded", function () {
const token = localStorage.getItem("admin_token");
if (token) {
showLogsSection();
loadLogs();
} else {
showLoginSection();
}
// Set current month as default
const currentMonthIndex = new Date().getMonth();
const months = [
"january",
"february",
"march",
"april",
"may",
"june",
"july",
"august",
"september",
"october",
"november",
"december",
];
document.getElementById("month").value = months[currentMonthIndex];
});
async function login(event) {
event.preventDefault();
const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
const loginBtn = document.getElementById("loginBtn");
const loginText = document.getElementById("loginText");
const loginLoading = document.getElementById("loginLoading");
const errorMessage = document.getElementById("errorMessage");
// Show loading state
loginBtn.disabled = true;
loginText.classList.add("hidden");
loginLoading.classList.remove("hidden");
errorMessage.classList.add("hidden");
try {
const response = await fetch(`${API_BASE}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
// Read raw text first to avoid 'Unexpected token <' when server returns HTML
const raw = await response.text();
let data = null;
try {
data = JSON.parse(raw);
} catch (e) {
// non-JSON response (HTML/error page) — keep raw for logging
console.warn("Non-JSON response:", raw);
}
console.log("Data response login:", data || raw);
if (!response.ok) {
const message =
(data && data.message) || `Request failed (${response.status})`;
throw new Error(message);
}
// Extract token supporting both shapes: { access_token } or { data: { access_token } }
const token =
(data && data.access_token) ||
(data && data.data && data.data.access_token);
if (!token) {
throw new Error("Login succeeded but no access token returned");
}
localStorage.setItem("admin_token", token);
// Show logs section
showLogsSection();
loadLogs();
} catch (error) {
errorMessage.textContent = error.message;
errorMessage.classList.remove("hidden");
} finally {
// Reset loading state
loginBtn.disabled = false;
loginText.classList.remove("hidden");
loginLoading.classList.add("hidden");
}
}
function showLoginSection() {
document.getElementById("loginSection").classList.remove("hidden");
document.getElementById("logsSection").classList.add("hidden");
}
function showLogsSection() {
document.getElementById("loginSection").classList.add("hidden");
document.getElementById("logsSection").classList.remove("hidden");
}
async function loadLogs(month = null) {
const logsContent = document.getElementById("logsContent");
const token = localStorage.getItem("admin_token");
if (!token) {
showLoginSection();
return;
}
const selectedMonth = month || document.getElementById("month").value;
try {
logsContent.innerHTML = `
<div class="text-center py-8">
<i class="fa-solid fa-spinner fa-spin text-2xl text-indigo-600 mb-3"></i>
<p class="text-gray-500">Loading logs...</p>
</div>
`;
const response = await fetch(`${LOGS_BASE}/${selectedMonth}`, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
// read raw and try parse JSON (avoid "Unexpected token '<'")
const raw = await response.text();
let payload = null;
try {
payload = JSON.parse(raw);
} catch (e) {
payload = raw; // fallback to raw HTML/text
console.warn("Non-JSON logs response:", raw);
}
if (response.status === 401 || response.status === 403) {
localStorage.removeItem("admin_token");
showLoginSection();
throw new Error("Session expired. Please login again.");
}
if (!response.ok) {
const message =
(payload && payload.message) ||
`Request failed (${response.status})`;
throw new Error(message);
}
// normalize logs array from possible response shapes
let logs = [];
if (Array.isArray(payload)) {
logs = payload;
} else if (
payload &&
payload.data &&
Array.isArray(payload.data.logs)
) {
logs = payload.data.logs;
} else if (payload && Array.isArray(payload.logs)) {
logs = payload.logs;
}
displayLogs(logs);
} catch (error) {
logsContent.innerHTML = `
<div class="error-message text-center">
<i class="fa-solid fa-exclamation-triangle mr-2"></i>
${error.message}
</div>
`;
}
}
function displayLogs(logsOrPayload) {
const logsContent = document.getElementById("logsContent");
// Normalize input ke array logs
let logs = [];
if (!logsOrPayload) {
logs = [];
} else if (Array.isArray(logsOrPayload)) {
logs = logsOrPayload;
} else if (
logsOrPayload.data &&
Array.isArray(logsOrPayload.data.logs)
) {
logs = logsOrPayload.data.logs;
} else if (Array.isArray(logsOrPayload.logs)) {
logs = logsOrPayload.logs;
} else if (typeof logsOrPayload === "object") {
// support wrapper like { month, logs, total } or { status, message, data: { logs } }
if (Array.isArray(logsOrPayload.logs)) logs = logsOrPayload.logs;
else if (logsOrPayload.data && Array.isArray(logsOrPayload.data.logs))
logs = logsOrPayload.data.logs;
else logs = [];
}
// Clear area
logsContent.innerHTML = "";
if (!logs || logs.length === 0) {
logsContent.innerHTML = `
<div class="p-8 glass-effect rounded-2xl text-center">
<i class="fa-solid fa-inbox text-4xl text-gray-300 mb-3"></i>
<p class="text-gray-500">No logs found for this month.</p>
</div>
`;
return;
}
const ul = document.createElement("ul");
ul.className = "space-y-6";
logs.forEach((log) => {
const li = document.createElement("li");
li.className =
"log-item p-6 glass-effect rounded-2xl hover:bg-gradient-to-r hover:from-indigo-50 hover:to-purple-50 transition-all";
const header = document.createElement("div");
header.className = "flex items-center gap-2 text-indigo-600 mb-3";
header.innerHTML = `<i class="fa-solid fa-terminal text-sm"></i><span class="font-semibold">Log Entry</span>`;
const pre = document.createElement("pre");
pre.className = "text-sm whitespace-pre-wrap break-words log-text";
// gunakan textContent agar aman dari HTML injection
if (typeof log === "string") {
pre.textContent = log;
} else {
try {
pre.textContent = JSON.stringify(log, null, 2);
} catch (e) {
pre.textContent = String(log);
}
}
li.appendChild(header);
li.appendChild(pre);
ul.appendChild(li);
});
logsContent.appendChild(ul);
}
function changeMonth() {
const selectedMonth = document.getElementById("month").value;
loadLogs(selectedMonth);
}
function refreshLogs() {
loadLogs();
}
function logout() {
localStorage.removeItem("admin_token");
showLoginSection();
// Clear form
document.getElementById("email").value = "";
document.getElementById("password").value = "";
document.getElementById("errorMessage").classList.add("hidden");
}
</script>
</body>
</html>

View File

@ -18,6 +18,7 @@ type (
LogsController interface {
GetLogs(ctx *gin.Context)
GetLogsByMonth(ctx *gin.Context)
ServeLogsPage(ctx *gin.Context)
}
logsController struct {
@ -32,6 +33,19 @@ func NewLogsController(injector *do.Injector) LogsController {
}
}
func (c *logsController) ServeLogsPage(ctx *gin.Context) {
acceptHeader := ctx.GetHeader("Accept")
if strings.Contains(acceptHeader, "text/html") {
month := strings.ToLower(time.Now().Format("January"))
ctx.HTML(http.StatusOK, "logs.html", gin.H{
"Month": month,
"Logs": []string{},
})
return
}
ctx.Next()
}
func (c *logsController) GetLogs(ctx *gin.Context) {
month := strings.ToLower(time.Now().Format("January"))
c.getLogsForMonth(ctx, month)
@ -74,6 +88,11 @@ func (c *logsController) getLogsForMonth(ctx *gin.Context, month string) {
logs = []string{}
}
// buat logs terbaru muncul paling atas (reverse slice)
for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 {
logs[i], logs[j] = logs[j], logs[i]
}
// Jika request Accept header adalah application/json, kembalikan JSON
if ctx.GetHeader("Accept") == "application/json" {
res := utils.BuildResponseSuccess("Success get logs", gin.H{

View File

@ -16,14 +16,27 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService)
// Public routes (tanpa auth) - untuk testing
publicLogs := server.Group("/logs")
{
publicLogs.GET("", logsController.GetLogs)
publicLogs.GET("/:month", logsController.GetLogsByMonth)
}
// publicLogs := server.Group("/logs")
// {
// publicLogs.GET("", logsController.GetLogs)
// publicLogs.GET("/:month", logsController.GetLogsByMonth)
// }
// Serve HTML page when browser requests text/html (no auth) — placed BEFORE protected group
// server.GET("/admin/logs", func(ctx *gin.Context) {
// acceptHeader := ctx.GetHeader("Accept")
// if strings.Contains(acceptHeader, "text/html") {
// ctx.HTML(http.StatusOK, "logs.html", gin.H{})
// return
// }
// // untuk API call lanjut ke handler berikutnya (mis. protected group)
// ctx.Next()
// })
server.GET("/admin/logs", logsController.ServeLogsPage)
// Protected routes (hanya superadmin)
protectedLogs := server.Group("/admin/logs")
protectedLogs := server.Group("/api/v1/logs")
protectedLogs.Use(middlewares.Authenticate(jwtService))
protectedLogs.Use(middlewares.RoleSuperAdmin(userService))
{

View File

@ -35,7 +35,15 @@ type (
}
)
// AssignPermissionsToRole implements RoleController.
// PingExample godoc
// @Summary ping example
// @Schemes
// @Description do ping
// @Tags example
// @Accept json
// @Produce json
// @Success 200 {string} Helloworld
// @Router /example/helloworld [get]
func (r *roleController) AssignPermissionsToRole(ctx *gin.Context) {
var req dto.AssignPermissionRequest
roleId := ctx.Param("id")

View File

@ -13,7 +13,6 @@ import (
"github.com/Caknoooo/go-pagination"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@ -45,6 +44,17 @@ func NewUserController(injector *do.Injector, us service.UserService) UserContro
}
}
// Register godoc
// @Summary Register a new user
// @Description Create a new user under the authenticated client. Validates input and returns created user.
// @Tags Users
// @Accept json
// @Produce json
// @Param body body dto.UserCreateRequest true "Register payload"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /users/register [post]
func (c *userController) Register(ctx *gin.Context) {
var user dto.UserCreateRequest
if err := ctx.ShouldBind(&user); err != nil {
@ -64,32 +74,52 @@ func (c *userController) Register(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// GetAllUser godoc
// @Summary Get list of users
// @Description Get paginated list of users for the current client. Supports filtering by name and including related roles.
// @Tags Users
// @Accept json
// @Produce json
// @Param name query string false "Filter by name (partial match)"
// @Param page query int false "Page number (default: 1)"
// @Param page_size query int false "Page size (default: 10)"
// @Param sort query string false "Sort expression, e.g. created_at desc"
// @Security ApiKeyAuth
// @Success 200 {array} dto.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Router /users [get]
func (c *userController) GetAllUser(ctx *gin.Context) {
clientId := ctx.MustGet("client_id").(string)
logrus.Info("Client ID: ", clientId)
var filter = &query.UserFilter{
ClientID: clientId,
Name: ctx.Query("name"),
Includes: []string{"Roles"}, // Tambahkan ini
Includes: []string{"Roles"},
}
filter.BindPagination(ctx)
ctx.ShouldBindQuery(filter)
logrus.Info("Filter sebelum query: ")
users, total, err := pagination.PaginatedQueryWithIncludable[query.M_User](c.db, filter)
// logrus.Info("Filter: ", filter)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
// Convert ke DTO
userResponses := ToUserResponses(users)
paginationResponse := pagination.CalculatePagination(filter.Pagination, total)
response := pagination.NewPaginatedResponse(http.StatusOK, dto.MESSAGE_SUCCESS_GET_LIST_USER, userResponses, paginationResponse)
ctx.JSON(http.StatusOK, response)
}
// Me godoc
// @Summary Get current user
// @Description Return the authenticated user's profile.
// @Tags Users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Router /users/me [get]
func (c *userController) Me(ctx *gin.Context) {
userId := ctx.MustGet("user_id").(string)
@ -104,6 +134,17 @@ func (c *userController) Me(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// GetUserById godoc
// @Summary Get user by ID
// @Description Get details of a user by their ID. Requires appropriate permissions.
// @Tags Users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Security ApiKeyAuth
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Router /users/{id} [get]
func (c *userController) GetUserById(ctx *gin.Context) {
userId := ctx.Param("id")
@ -118,6 +159,16 @@ func (c *userController) GetUserById(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// Login godoc
// @Summary User login
// @Description Authenticate user and return access & refresh tokens.
// @Tags Auth
// @Accept json
// @Produce json
// @Param body body dto.UserLoginRequest true "Login payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /auth/login [post]
func (c *userController) Login(ctx *gin.Context) {
var req dto.UserLoginRequest
if err := ctx.ShouldBind(&req); err != nil {
@ -137,6 +188,16 @@ func (c *userController) Login(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// SendVerificationEmail godoc
// @Summary Send verification email
// @Description Send email with verification code/link to a user's email address.
// @Tags Users
// @Accept json
// @Produce json
// @Param body body dto.SendVerificationEmailRequest true "Email request payload"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /users/send-verification-email [post]
func (c *userController) SendVerificationEmail(ctx *gin.Context) {
var req dto.SendVerificationEmailRequest
if err := ctx.ShouldBind(&req); err != nil {
@ -156,6 +217,16 @@ func (c *userController) SendVerificationEmail(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// VerifyEmail godoc
// @Summary Verify user email
// @Description Verify a user's email using code or token sent via email.
// @Tags Users
// @Accept json
// @Produce json
// @Param body body dto.VerifyEmailRequest true "Verify email payload"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Router /users/verify-email [post]
func (c *userController) VerifyEmail(ctx *gin.Context) {
var req dto.VerifyEmailRequest
if err := ctx.ShouldBind(&req); err != nil {
@ -175,6 +246,17 @@ func (c *userController) VerifyEmail(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// Update godoc
// @Summary Update current user
// @Description Update profile of the authenticated user. Use multipart/form-data if uploading files (photo).
// @Tags Users
// @Accept json
// @Produce json
// @Param body body dto.UserUpdateRequest true "Update payload"
// @Security ApiKeyAuth
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]interface{}
// @Router /users [put]
func (c *userController) Update(ctx *gin.Context) {
var req dto.UserUpdateRequest
if err := ctx.ShouldBind(&req); err != nil {
@ -195,6 +277,16 @@ func (c *userController) Update(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// Delete godoc
// @Summary Delete current user
// @Description Delete the authenticated user's account (soft/hard depends on implementation).
// @Tags Users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /users [delete]
func (c *userController) Delete(ctx *gin.Context) {
userId := ctx.MustGet("user_id").(string)
@ -208,6 +300,17 @@ func (c *userController) Delete(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
// Refresh godoc
// @Summary Refresh auth token
// @Description Exchange a refresh token for a new access token (and optionally a new refresh token).
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body object true "Refresh token payload" example:{"refresh_token":"string"}
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Router /auth/refresh [post]
func (c *userController) Refresh(ctx *gin.Context) {
var req authDto.RefreshTokenRequest
if err := ctx.ShouldBind(&req); err != nil {