diff --git a/LAYOUT.md b/LAYOUT.md new file mode 100644 index 00000000..18b21ccd --- /dev/null +++ b/LAYOUT.md @@ -0,0 +1,289 @@ +# ๐Ÿ“Š API Service Project Layout + +## ๐ŸŽฏ Project Overview Dashboard + +### ๐Ÿ“ˆ Key Metrics +- **Language**: Go 1.24.4 +- **Framework**: Gin 1.10.1 +- **Database**: PostgreSQL with GORM +- **Authentication**: JWT + Keycloak +- **Documentation**: Swagger/OpenAPI 3.0 +- **Container**: Docker & Docker Compose + +### ๐Ÿ—๏ธ Architecture Overview +``` +api-service/ +โ”œโ”€โ”€ cmd/ +โ”‚ โ””โ”€โ”€ api/ +โ”‚ โ””โ”€โ”€ main.go +โ”œโ”€โ”€ internal/ +โ”‚ โ”œโ”€โ”€ config/ # Config loader +โ”‚ โ”œโ”€โ”€ server/ # HTTP server setup + routes binding +โ”‚ โ”œโ”€โ”€ handler/ # HTTP handlers (controller layer) +โ”‚ โ”œโ”€โ”€ service/ # Business logic +โ”‚ โ”œโ”€โ”€ repository/ # DB access layer +โ”‚ โ”œโ”€โ”€ model/ # App/domain models +โ”‚ โ””โ”€โ”€ middleware/ # HTTP middlewares +โ”œโ”€โ”€ pkg/ # Reusable packages independent of business logic +โ”‚ โ”œโ”€โ”€ logger/ +โ”‚ โ”œโ”€โ”€ utils/ +โ”‚ โ””โ”€โ”€ validator/ +โ”œโ”€โ”€ scripts/ # Deployment & build automation +โ””โ”€โ”€ docs/ # Documentation (incl. swagger) +``` + +## ๐Ÿš€ Development Environment Layout + +### ๐Ÿ“‹ Prerequisites Setup +```bash +# Required tools +- Go 1.24.4+ +- PostgreSQL 12+ +- Docker & Docker Compose +- Keycloak Server (optional) + +# Development tools +- Air (hot reload) +- Swag (Swagger docs) +- Make (build automation) +``` + +### ๐Ÿ”ง Configuration Files Layout +``` +project-root/ +โ”œโ”€โ”€ .env # Environment variables +โ”œโ”€โ”€ .air.toml # Air configuration +โ”œโ”€โ”€ Makefile # Build commands +โ”œโ”€โ”€ docker-compose.yml # Docker services +โ”œโ”€โ”€ Dockerfile # Container image +โ””โ”€โ”€ .goreleaser.yml # Release configuration +``` + +## ๐Ÿ“š API Documentation Layout + +### ๐ŸŒ Base URL Structure +``` +http://localhost:8080/api/v1 +``` + +### ๐Ÿ“– Endpoint Categories + +#### ๐Ÿ” Health & Monitoring +- `GET /api/v1/health` - Health check +- `GET /api/v1/` - Hello world +- `GET /api/v1/metrics` - Application metrics + +#### ๐Ÿ” Authentication Endpoints +- `POST /api/v1/auth/login` - User login +- `POST /api/v1/auth/register` - User registration +- `POST /api/v1/auth/refresh` - Token refresh +- `POST /api/v1/auth/logout` - User logout + +#### ๐Ÿ‘ค User Management +- `GET /api/v1/users/profile` - Get user profile +- `PUT /api/v1/users/profile` - Update user profile +- `GET /api/v1/users` - List users (admin) +- `GET /api/v1/users/:id` - Get user by ID + +#### ๐Ÿ“ฆ Product Management +- `GET /api/v1/products` - List products +- `POST /api/v1/products` - Create product +- `GET /api/v1/products/:id` - Get product +- `PUT /api/v1/products/:id` - Update product +- `DELETE /api/v1/products/:id` - Delete product + +#### ๐Ÿ”„ Real-time Features +- `GET /api/v1/websocket` - WebSocket connection +- `GET /api/v1/webservice` - Web service endpoint + +### ๐Ÿ“Š Swagger Documentation +- **Interactive UI**: http://localhost:8080/swagger/index.html +- **OpenAPI JSON**: http://localhost:8080/swagger/doc.json + +## ๐Ÿณ Docker Deployment Layout + +### ๐Ÿƒ Development Environment +```bash +# Start all services +make docker-run + +# Services included: +- PostgreSQL database +- API service with hot reload +- pgAdmin (database management) +``` + +### ๐Ÿš€ Production Environment +```bash +# Build production image +docker build -t api-service:prod . + +# Run production container +docker run -d \ + -p 8080:8080 \ + --env-file .env \ + --name api-service \ + api-service:prod +``` + +### ๐Ÿ“ฆ Docker Compose Services +```yaml +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_DB: api_service + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + api: + build: . + ports: + - "8080:8080" + depends_on: + - postgres + environment: + - BLUEPRINT_DB_HOST=postgres + volumes: + - .:/app + command: air -c .air.toml +``` + +## ๐Ÿ› ๏ธ Development Workflow Layout + +### ๐Ÿ”„ Daily Development Cycle +```bash +# 1. Start development environment +make watch + +# 2. Run tests +make test + +# 3. Check code quality +make lint + +# 4. Build for production +make build +``` + +### ๐Ÿงช Testing Strategy +``` +tests/ +โ”œโ”€โ”€ unit/ # Unit tests +โ”œโ”€โ”€ integration/ # Integration tests +โ”œโ”€โ”€ e2e/ # End-to-end tests +โ””โ”€โ”€ fixtures/ # Test data +``` + +### ๐Ÿ“Š Monitoring & Logging +- **Health Check**: `/api/v1/health` +- **Metrics**: Prometheus metrics +- **Logging**: Structured JSON logs +- **Tracing**: Distributed tracing support + +## ๐Ÿ“ File Structure Detailed Layout + +### ๐Ÿ“„ Configuration Files +``` +.env.example # Environment template +config/ +โ”œโ”€โ”€ config.go # Configuration struct +โ”œโ”€โ”€ config_test.go # Configuration tests +โ””โ”€โ”€ validation.go # Config validation +``` + +### ๐Ÿ—„๏ธ Database Layout +``` +database/ +โ”œโ”€โ”€ migrations/ # Database migrations +โ”œโ”€โ”€ seeds/ # Seed data +โ”œโ”€โ”€ models/ # GORM models +โ””โ”€โ”€ repositories/ # Data access layer +``` + +### ๐ŸŽฏ Handler Layout +``` +handlers/ +โ”œโ”€โ”€ auth.go # Authentication handlers +โ”œโ”€โ”€ user.go # User management handlers +โ”œโ”€โ”€ product.go # Product handlers +โ”œโ”€โ”€ health.go # Health check handlers +โ””โ”€โ”€ middleware/ # HTTP middleware + โ”œโ”€โ”€ auth.go # JWT middleware + โ”œโ”€โ”€ cors.go # CORS middleware + โ””โ”€โ”€ logger.go # Logging middleware +``` + +### ๐ŸŒ Route Layout +``` +routes/ +โ”œโ”€โ”€ v1/ +โ”‚ โ”œโ”€โ”€ routes.go # Route definitions +โ”‚ โ”œโ”€โ”€ auth_routes.go # Auth routes +โ”‚ โ”œโ”€โ”€ user_routes.go # User routes +โ”‚ โ””โ”€โ”€ product_routes.go # Product routes +โ””โ”€โ”€ middleware.go # Route middleware +``` + +## ๐Ÿš€ Quick Start Commands + +### ๐Ÿƒ Development +```bash +# Clone and setup +git clone +cd api-service +cp .env.example .env +make docker-run + +# Start development +make watch +``` + +### ๐Ÿงช Testing +```bash +# Run all tests +make all + +# Run specific test +make test +make itest +``` + +### ๐Ÿš€ Production +```bash +# Build and run +make build +./main.exe +``` + +## ๐Ÿ“Š Performance Monitoring Layout + +### ๐Ÿ“ˆ Metrics Collection +- **Request Duration**: Track API response times +- **Error Rates**: Monitor error frequencies +- **Database Performance**: Query execution times +- **Memory Usage**: Application memory consumption + +### ๐Ÿ” Health Checks +- **Database Connectivity**: PostgreSQL connection status +- **External Services**: Keycloak availability +- **Resource Usage**: CPU and memory utilization + +## ๐ŸŽฏ Next Steps + +1. **Setup Environment**: Copy `.env.example` to `.env` +2. **Start Database**: Run `make docker-run` +3. **Start Development**: Run `make watch` +4. **Test API**: Visit `http://localhost:8080/swagger/index.html` +5. **Run Tests**: Execute `make all` + +## ๐Ÿ“ž Support & Resources + +- **Documentation**: Check `/docs` folder +- **API Testing**: Use Swagger UI at `/swagger/index.html` +- **Database**: Access pgAdmin at `http://localhost:5050` +- **Issues**: Create GitHub issues for bugs/features diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go deleted file mode 100644 index f628fda6..00000000 --- a/cmd/api/docs/docs.go +++ /dev/null @@ -1,833 +0,0 @@ -// 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": { - "/": { - "get": { - "description": "Returns a hello world message", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "root" - ], - "summary": "Hello World endpoint", - "responses": { - "200": { - "description": "Hello world message", - "schema": { - "$ref": "#/definitions/models.HelloWorldResponse" - } - } - } - } - }, - "/api/v1/auth/login": { - "post": { - "description": "Authenticate user with username and password to receive JWT token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Login user and get JWT token", - "parameters": [ - { - "description": "Login credentials", - "name": "login", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/auth/me": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get information about the currently authenticated user", - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Get current user info", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.User" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/auth/refresh": { - "post": { - "description": "Refresh the JWT token using a valid refresh token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Refresh JWT token", - "parameters": [ - { - "description": "Refresh token", - "name": "refresh", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/auth/register": { - "post": { - "description": "Register a new user account", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Register new user", - "parameters": [ - { - "description": "Registration data", - "name": "register", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/example": { - "get": { - "description": "Returns a simple message for GET request", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "example" - ], - "summary": "Example GET service", - "responses": { - "200": { - "description": "Example GET response", - "schema": { - "$ref": "#/definitions/models.ExampleGetResponse" - } - } - } - }, - "post": { - "description": "Accepts a JSON payload and returns a response with an ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "example" - ], - "summary": "Example POST service", - "parameters": [ - { - "description": "Example POST request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.ExamplePostRequest" - } - } - ], - "responses": { - "200": { - "description": "Example POST response", - "schema": { - "$ref": "#/definitions/models.ExamplePostResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/api/v1/products": { - "get": { - "description": "Returns a list of products", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Get product", - "responses": { - "200": { - "description": "Product GET response", - "schema": { - "$ref": "#/definitions/models.ProductGetResponse" - } - } - } - }, - "post": { - "description": "Creates a new product", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Create product", - "parameters": [ - { - "description": "Product creation request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.ProductCreateRequest" - } - } - ], - "responses": { - "201": { - "description": "Product created successfully", - "schema": { - "$ref": "#/definitions/models.ProductCreateResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/api/v1/products/{id}": { - "get": { - "description": "Returns a single product by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Get product by ID", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product GET by ID response", - "schema": { - "$ref": "#/definitions/models.ProductGetByIDResponse" - } - }, - "404": { - "description": "Product not found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - }, - "put": { - "description": "Updates an existing product", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Update product", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Product update request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.ProductUpdateRequest" - } - } - ], - "responses": { - "200": { - "description": "Product updated successfully", - "schema": { - "$ref": "#/definitions/models.ProductUpdateResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "404": { - "description": "Product not found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - }, - "delete": { - "description": "Deletes a product by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Delete product", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product deleted successfully", - "schema": { - "$ref": "#/definitions/models.ProductDeleteResponse" - } - }, - "404": { - "description": "Product not found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/api/v1/token/generate": { - "post": { - "description": "Generate a JWT token for a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Token" - ], - "summary": "Generate JWT token", - "parameters": [ - { - "description": "User credentials", - "name": "token", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/token/generate-direct": { - "post": { - "description": "Generate a JWT token directly without password verification (for testing)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Token" - ], - "summary": "Generate token directly", - "parameters": [ - { - "description": "User info", - "name": "user", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/health": { - "get": { - "description": "Returns the health status of the API service", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "health" - ], - "summary": "Health check endpoint", - "responses": { - "200": { - "description": "Health status", - "schema": { - "$ref": "#/definitions/models.HealthResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - } - }, - "definitions": { - "models.ErrorResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "error": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ExampleGetResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "models.ExamplePostRequest": { - "type": "object", - "required": [ - "age", - "name" - ], - "properties": { - "age": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "models.ExamplePostResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.HealthResponse": { - "type": "object", - "properties": { - "details": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "status": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "models.HelloWorldResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "models.LoginRequest": { - "type": "object", - "required": [ - "password", - "username" - ], - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "models.ProductCreateRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, - "models.ProductCreateResponse": { - "type": "object", - "properties": { - "data": {}, - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ProductDeleteResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ProductGetByIDResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ProductGetResponse": { - "type": "object", - "properties": { - "data": {}, - "message": { - "type": "string" - } - } - }, - "models.ProductUpdateRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, - "models.ProductUpdateResponse": { - "type": "object", - "properties": { - "data": {}, - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.TokenResponse": { - "type": "object", - "properties": { - "access_token": { - "type": "string" - }, - "expires_in": { - "type": "integer" - }, - "token_type": { - "type": "string" - } - } - }, - "models.User": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "role": { - "type": "string" - }, - "username": { - "type": "string" - } - } - } - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "1.0.0", - Host: "localhost:8080", - BasePath: "/api/v1", - Schemes: []string{"http", "https"}, - Title: "API Service", - Description: "A comprehensive Go API service with Swagger documentation", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json deleted file mode 100644 index de164848..00000000 --- a/cmd/api/docs/swagger.json +++ /dev/null @@ -1,814 +0,0 @@ -{ - "schemes": [ - "http", - "https" - ], - "swagger": "2.0", - "info": { - "description": "A comprehensive Go API service with Swagger documentation", - "title": "API Service", - "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.0" - }, - "host": "localhost:8080", - "basePath": "/api/v1", - "paths": { - "/": { - "get": { - "description": "Returns a hello world message", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "root" - ], - "summary": "Hello World endpoint", - "responses": { - "200": { - "description": "Hello world message", - "schema": { - "$ref": "#/definitions/models.HelloWorldResponse" - } - } - } - } - }, - "/api/v1/auth/login": { - "post": { - "description": "Authenticate user with username and password to receive JWT token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Login user and get JWT token", - "parameters": [ - { - "description": "Login credentials", - "name": "login", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/auth/me": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get information about the currently authenticated user", - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Get current user info", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.User" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/auth/refresh": { - "post": { - "description": "Refresh the JWT token using a valid refresh token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Refresh JWT token", - "parameters": [ - { - "description": "Refresh token", - "name": "refresh", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/auth/register": { - "post": { - "description": "Register a new user account", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Authentication" - ], - "summary": "Register new user", - "parameters": [ - { - "description": "Registration data", - "name": "register", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/example": { - "get": { - "description": "Returns a simple message for GET request", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "example" - ], - "summary": "Example GET service", - "responses": { - "200": { - "description": "Example GET response", - "schema": { - "$ref": "#/definitions/models.ExampleGetResponse" - } - } - } - }, - "post": { - "description": "Accepts a JSON payload and returns a response with an ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "example" - ], - "summary": "Example POST service", - "parameters": [ - { - "description": "Example POST request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.ExamplePostRequest" - } - } - ], - "responses": { - "200": { - "description": "Example POST response", - "schema": { - "$ref": "#/definitions/models.ExamplePostResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/api/v1/products": { - "get": { - "description": "Returns a list of products", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Get product", - "responses": { - "200": { - "description": "Product GET response", - "schema": { - "$ref": "#/definitions/models.ProductGetResponse" - } - } - } - }, - "post": { - "description": "Creates a new product", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Create product", - "parameters": [ - { - "description": "Product creation request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.ProductCreateRequest" - } - } - ], - "responses": { - "201": { - "description": "Product created successfully", - "schema": { - "$ref": "#/definitions/models.ProductCreateResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/api/v1/products/{id}": { - "get": { - "description": "Returns a single product by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Get product by ID", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product GET by ID response", - "schema": { - "$ref": "#/definitions/models.ProductGetByIDResponse" - } - }, - "404": { - "description": "Product not found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - }, - "put": { - "description": "Updates an existing product", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Update product", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Product update request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.ProductUpdateRequest" - } - } - ], - "responses": { - "200": { - "description": "Product updated successfully", - "schema": { - "$ref": "#/definitions/models.ProductUpdateResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - }, - "404": { - "description": "Product not found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - }, - "delete": { - "description": "Deletes a product by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "product" - ], - "summary": "Delete product", - "parameters": [ - { - "type": "string", - "description": "Product ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product deleted successfully", - "schema": { - "$ref": "#/definitions/models.ProductDeleteResponse" - } - }, - "404": { - "description": "Product not found", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - }, - "/api/v1/token/generate": { - "post": { - "description": "Generate a JWT token for a user", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Token" - ], - "summary": "Generate JWT token", - "parameters": [ - { - "description": "User credentials", - "name": "token", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/v1/token/generate-direct": { - "post": { - "description": "Generate a JWT token directly without password verification (for testing)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Token" - ], - "summary": "Generate token directly", - "parameters": [ - { - "description": "User info", - "name": "user", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.TokenResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/health": { - "get": { - "description": "Returns the health status of the API service", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "health" - ], - "summary": "Health check endpoint", - "responses": { - "200": { - "description": "Health status", - "schema": { - "$ref": "#/definitions/models.HealthResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/models.ErrorResponse" - } - } - } - } - } - }, - "definitions": { - "models.ErrorResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "error": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ExampleGetResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "models.ExamplePostRequest": { - "type": "object", - "required": [ - "age", - "name" - ], - "properties": { - "age": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "models.ExamplePostResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.HealthResponse": { - "type": "object", - "properties": { - "details": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "status": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "models.HelloWorldResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "models.LoginRequest": { - "type": "object", - "required": [ - "password", - "username" - ], - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "models.ProductCreateRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, - "models.ProductCreateResponse": { - "type": "object", - "properties": { - "data": {}, - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ProductDeleteResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ProductGetByIDResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.ProductGetResponse": { - "type": "object", - "properties": { - "data": {}, - "message": { - "type": "string" - } - } - }, - "models.ProductUpdateRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, - "models.ProductUpdateResponse": { - "type": "object", - "properties": { - "data": {}, - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "models.TokenResponse": { - "type": "object", - "properties": { - "access_token": { - "type": "string" - }, - "expires_in": { - "type": "integer" - }, - "token_type": { - "type": "string" - } - } - }, - "models.User": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "role": { - "type": "string" - }, - "username": { - "type": "string" - } - } - } - } -} \ No newline at end of file diff --git a/cmd/api/docs/swagger.yaml b/cmd/api/docs/swagger.yaml deleted file mode 100644 index b190e41b..00000000 --- a/cmd/api/docs/swagger.yaml +++ /dev/null @@ -1,534 +0,0 @@ -basePath: /api/v1 -definitions: - models.ErrorResponse: - properties: - code: - type: integer - error: - type: string - message: - type: string - type: object - models.ExampleGetResponse: - properties: - message: - type: string - type: object - models.ExamplePostRequest: - properties: - age: - type: integer - name: - type: string - required: - - age - - name - type: object - models.ExamplePostResponse: - properties: - id: - type: string - message: - type: string - type: object - models.HealthResponse: - properties: - details: - additionalProperties: - type: string - type: object - status: - type: string - timestamp: - type: string - type: object - models.HelloWorldResponse: - properties: - message: - type: string - version: - type: string - type: object - models.LoginRequest: - properties: - password: - type: string - username: - type: string - required: - - password - - username - type: object - models.ProductCreateRequest: - properties: - name: - type: string - required: - - name - type: object - models.ProductCreateResponse: - properties: - data: {} - id: - type: string - message: - type: string - type: object - models.ProductDeleteResponse: - properties: - id: - type: string - message: - type: string - type: object - models.ProductGetByIDResponse: - properties: - id: - type: string - message: - type: string - type: object - models.ProductGetResponse: - properties: - data: {} - message: - type: string - type: object - models.ProductUpdateRequest: - properties: - name: - type: string - required: - - name - type: object - models.ProductUpdateResponse: - properties: - data: {} - id: - type: string - message: - type: string - type: object - models.TokenResponse: - properties: - access_token: - type: string - expires_in: - type: integer - token_type: - type: string - type: object - models.User: - properties: - email: - type: string - id: - type: string - role: - type: string - username: - type: string - type: object -host: localhost:8080 -info: - contact: - email: support@swagger.io - name: API Support - url: http://www.swagger.io/support - description: A comprehensive Go API service with Swagger documentation - license: - name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - termsOfService: http://swagger.io/terms/ - title: API Service - version: 1.0.0 -paths: - /: - get: - consumes: - - application/json - description: Returns a hello world message - produces: - - application/json - responses: - "200": - description: Hello world message - schema: - $ref: '#/definitions/models.HelloWorldResponse' - summary: Hello World endpoint - tags: - - root - /api/v1/auth/login: - post: - consumes: - - application/json - description: Authenticate user with username and password to receive JWT token - parameters: - - description: Login credentials - in: body - name: login - required: true - schema: - $ref: '#/definitions/models.LoginRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.TokenResponse' - "400": - description: Bad request - schema: - additionalProperties: - type: string - type: object - "401": - description: Unauthorized - schema: - additionalProperties: - type: string - type: object - summary: Login user and get JWT token - tags: - - Authentication - /api/v1/auth/me: - get: - description: Get information about the currently authenticated user - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.User' - "401": - description: Unauthorized - schema: - additionalProperties: - type: string - type: object - security: - - Bearer: [] - summary: Get current user info - tags: - - Authentication - /api/v1/auth/refresh: - post: - consumes: - - application/json - description: Refresh the JWT token using a valid refresh token - parameters: - - description: Refresh token - in: body - name: refresh - required: true - schema: - additionalProperties: - type: string - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.TokenResponse' - "400": - description: Bad request - schema: - additionalProperties: - type: string - type: object - "401": - description: Unauthorized - schema: - additionalProperties: - type: string - type: object - summary: Refresh JWT token - tags: - - Authentication - /api/v1/auth/register: - post: - consumes: - - application/json - description: Register a new user account - parameters: - - description: Registration data - in: body - name: register - required: true - schema: - additionalProperties: - type: string - type: object - produces: - - application/json - responses: - "201": - description: Created - schema: - additionalProperties: - type: string - type: object - "400": - description: Bad request - schema: - additionalProperties: - type: string - type: object - summary: Register new user - tags: - - Authentication - /api/v1/example: - get: - consumes: - - application/json - description: Returns a simple message for GET request - produces: - - application/json - responses: - "200": - description: Example GET response - schema: - $ref: '#/definitions/models.ExampleGetResponse' - summary: Example GET service - tags: - - example - post: - consumes: - - application/json - description: Accepts a JSON payload and returns a response with an ID - parameters: - - description: Example POST request - in: body - name: request - required: true - schema: - $ref: '#/definitions/models.ExamplePostRequest' - produces: - - application/json - responses: - "200": - description: Example POST response - schema: - $ref: '#/definitions/models.ExamplePostResponse' - "400": - description: Bad request - schema: - $ref: '#/definitions/models.ErrorResponse' - summary: Example POST service - tags: - - example - /api/v1/products: - get: - consumes: - - application/json - description: Returns a list of products - produces: - - application/json - responses: - "200": - description: Product GET response - schema: - $ref: '#/definitions/models.ProductGetResponse' - summary: Get product - tags: - - product - post: - consumes: - - application/json - description: Creates a new product - parameters: - - description: Product creation request - in: body - name: request - required: true - schema: - $ref: '#/definitions/models.ProductCreateRequest' - produces: - - application/json - responses: - "201": - description: Product created successfully - schema: - $ref: '#/definitions/models.ProductCreateResponse' - "400": - description: Bad request - schema: - $ref: '#/definitions/models.ErrorResponse' - summary: Create product - tags: - - product - /api/v1/products/{id}: - delete: - consumes: - - application/json - description: Deletes a product by ID - parameters: - - description: Product ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Product deleted successfully - schema: - $ref: '#/definitions/models.ProductDeleteResponse' - "404": - description: Product not found - schema: - $ref: '#/definitions/models.ErrorResponse' - summary: Delete product - tags: - - product - get: - consumes: - - application/json - description: Returns a single product by ID - parameters: - - description: Product ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Product GET by ID response - schema: - $ref: '#/definitions/models.ProductGetByIDResponse' - "404": - description: Product not found - schema: - $ref: '#/definitions/models.ErrorResponse' - summary: Get product by ID - tags: - - product - put: - consumes: - - application/json - description: Updates an existing product - parameters: - - description: Product ID - in: path - name: id - required: true - type: string - - description: Product update request - in: body - name: request - required: true - schema: - $ref: '#/definitions/models.ProductUpdateRequest' - produces: - - application/json - responses: - "200": - description: Product updated successfully - schema: - $ref: '#/definitions/models.ProductUpdateResponse' - "400": - description: Bad request - schema: - $ref: '#/definitions/models.ErrorResponse' - "404": - description: Product not found - schema: - $ref: '#/definitions/models.ErrorResponse' - summary: Update product - tags: - - product - /api/v1/token/generate: - post: - consumes: - - application/json - description: Generate a JWT token for a user - parameters: - - description: User credentials - in: body - name: token - required: true - schema: - $ref: '#/definitions/models.LoginRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.TokenResponse' - "400": - description: Bad request - schema: - additionalProperties: - type: string - type: object - "401": - description: Unauthorized - schema: - additionalProperties: - type: string - type: object - summary: Generate JWT token - tags: - - Token - /api/v1/token/generate-direct: - post: - consumes: - - application/json - description: Generate a JWT token directly without password verification (for - testing) - parameters: - - description: User info - in: body - name: user - required: true - schema: - additionalProperties: - type: string - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.TokenResponse' - "400": - description: Bad request - schema: - additionalProperties: - type: string - type: object - summary: Generate token directly - tags: - - Token - /health: - get: - consumes: - - application/json - description: Returns the health status of the API service - produces: - - application/json - responses: - "200": - description: Health status - schema: - $ref: '#/definitions/models.HealthResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/models.ErrorResponse' - summary: Health check endpoint - tags: - - health -schemes: -- http -- https -swagger: "2.0" diff --git a/cmd/api/main.go b/cmd/api/main.go index a09e7d61..b1718f75 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -11,7 +11,7 @@ import ( "api-service/internal/server" - _ "api-service/cmd/api/docs" + _ "api-service/docs" ) // @title API Service diff --git a/docs/docs.go b/docs/docs.go index 5b664b14..f628fda6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -24,6 +24,7 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", + "paths": { "/": { "get": { "description": "Returns a hello world message", @@ -47,6 +48,196 @@ const docTemplate = `{ } } }, + "/api/v1/auth/login": { + "post": { + "description": "Authenticate user with username and password to receive JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Login user and get JWT token", + "parameters": [ + { + "description": "Login credentials", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the currently authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Get current user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "Refresh the JWT token using a valid refresh token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh JWT token", + "parameters": [ + { + "description": "Refresh token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "Register a new user account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Register new user", + "parameters": [ + { + "description": "Registration data", + "name": "register", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/example": { "get": { "description": "Returns a simple message for GET request", @@ -294,6 +485,104 @@ const docTemplate = `{ } } }, + "/api/v1/token/generate": { + "post": { + "description": "Generate a JWT token for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate JWT token", + "parameters": [ + { + "description": "User credentials", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/token/generate-direct": { + "post": { + "description": "Generate a JWT token directly without password verification (for testing)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate token directly", + "parameters": [ + { + "description": "User info", + "name": "user", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/health": { "get": { "description": "Returns the health status of the API service", @@ -401,6 +690,21 @@ const docTemplate = `{ } } }, + "models.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "models.ProductCreateRequest": { "type": "object", "required": [ @@ -477,6 +781,37 @@ const docTemplate = `{ "type": "string" } } + }, + "models.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "token_type": { + "type": "string" + } + } + }, + "models.User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "username": { + "type": "string" + } + } } } }` diff --git a/docs/swagger.json b/docs/swagger.json index 01963090..de164848 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -45,6 +45,196 @@ } } }, + "/api/v1/auth/login": { + "post": { + "description": "Authenticate user with username and password to receive JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Login user and get JWT token", + "parameters": [ + { + "description": "Login credentials", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get information about the currently authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Get current user info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "Refresh the JWT token using a valid refresh token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Refresh JWT token", + "parameters": [ + { + "description": "Refresh token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "Register a new user account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authentication" + ], + "summary": "Register new user", + "parameters": [ + { + "description": "Registration data", + "name": "register", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/example": { "get": { "description": "Returns a simple message for GET request", @@ -292,6 +482,104 @@ } } }, + "/api/v1/token/generate": { + "post": { + "description": "Generate a JWT token for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate JWT token", + "parameters": [ + { + "description": "User credentials", + "name": "token", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/token/generate-direct": { + "post": { + "description": "Generate a JWT token directly without password verification (for testing)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Token" + ], + "summary": "Generate token directly", + "parameters": [ + { + "description": "User info", + "name": "user", + "in": "body", + "required": true, + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TokenResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/health": { "get": { "description": "Returns the health status of the API service", @@ -399,6 +687,21 @@ } } }, + "models.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, "models.ProductCreateRequest": { "type": "object", "required": [ @@ -475,6 +778,37 @@ "type": "string" } } + }, + "models.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "token_type": { + "type": "string" + } + } + }, + "models.User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "username": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e478bcaa..b190e41b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -49,6 +49,16 @@ definitions: version: type: string type: object + models.LoginRequest: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object models.ProductCreateRequest: properties: name: @@ -99,6 +109,26 @@ definitions: message: type: string type: object + models.TokenResponse: + properties: + access_token: + type: string + expires_in: + type: integer + token_type: + type: string + type: object + models.User: + properties: + email: + type: string + id: + type: string + role: + type: string + username: + type: string + type: object host: localhost:8080 info: contact: @@ -128,6 +158,129 @@ paths: summary: Hello World endpoint tags: - root + /api/v1/auth/login: + post: + consumes: + - application/json + description: Authenticate user with username and password to receive JWT token + parameters: + - description: Login credentials + in: body + name: login + required: true + schema: + $ref: '#/definitions/models.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Login user and get JWT token + tags: + - Authentication + /api/v1/auth/me: + get: + description: Get information about the currently authenticated user + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + security: + - Bearer: [] + summary: Get current user info + tags: + - Authentication + /api/v1/auth/refresh: + post: + consumes: + - application/json + description: Refresh the JWT token using a valid refresh token + parameters: + - description: Refresh token + in: body + name: refresh + required: true + schema: + additionalProperties: + type: string + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Refresh JWT token + tags: + - Authentication + /api/v1/auth/register: + post: + consumes: + - application/json + description: Register a new user account + parameters: + - description: Registration data + in: body + name: register + required: true + schema: + additionalProperties: + type: string + type: object + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + summary: Register new user + tags: + - Authentication /api/v1/example: get: consumes: @@ -291,6 +444,71 @@ paths: summary: Update product tags: - product + /api/v1/token/generate: + post: + consumes: + - application/json + description: Generate a JWT token for a user + parameters: + - description: User credentials + in: body + name: token + required: true + schema: + $ref: '#/definitions/models.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Generate JWT token + tags: + - Token + /api/v1/token/generate-direct: + post: + consumes: + - application/json + description: Generate a JWT token directly without password verification (for + testing) + parameters: + - description: User info + in: body + name: user + required: true + schema: + additionalProperties: + type: string + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.TokenResponse' + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + summary: Generate token directly + tags: + - Token /health: get: consumes: diff --git a/internal/handlers/auth.go b/internal/handlers/auth/auth.go similarity index 97% rename from internal/handlers/auth.go rename to internal/handlers/auth/auth.go index 7c5be4b7..3bd74ddc 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth/auth.go @@ -1,8 +1,8 @@ package handlers import ( - "api-service/internal/models" - "api-service/internal/services" + models "api-service/internal/models/auth" + services "api-service/internal/services/auth" "net/http" "github.com/gin-gonic/gin" diff --git a/internal/handlers/token_handler.go b/internal/handlers/auth/token.go similarity index 96% rename from internal/handlers/token_handler.go rename to internal/handlers/auth/token.go index 5cce937f..02383c70 100644 --- a/internal/handlers/token_handler.go +++ b/internal/handlers/auth/token.go @@ -1,8 +1,8 @@ package handlers import ( - "api-service/internal/models" - "api-service/internal/services" + models "api-service/internal/models/auth" + services "api-service/internal/services/auth" "net/http" "github.com/gin-gonic/gin" diff --git a/internal/handlers/example.go b/internal/handlers/component/example.go similarity index 100% rename from internal/handlers/example.go rename to internal/handlers/component/example.go diff --git a/internal/handlers/health.go b/internal/handlers/component/health.go similarity index 100% rename from internal/handlers/health.go rename to internal/handlers/component/health.go diff --git a/internal/handlers/product.go b/internal/handlers/component/product.go similarity index 100% rename from internal/handlers/product.go rename to internal/handlers/component/product.go diff --git a/internal/handlers/employee/employee.go b/internal/handlers/employee/employee.go new file mode 100644 index 00000000..72d322a6 --- /dev/null +++ b/internal/handlers/employee/employee.go @@ -0,0 +1,121 @@ +package handlers + +import ( + "net/http" + + "api-service/internal/models/employee" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// EmployeeHandler handles employee services +type EmployeeHandler struct{} + +// NewEmployeeHandler creates a new EmployeeHandler +func NewEmployeeHandler() *EmployeeHandler { + return &%!s(MISSING)Handler{} +} +// GetEmployee godoc +// @Summary Get employee +// @Description Returns a list of employees +// @Tags employee +// @Accept json +// @Produce json +// @Success 200 {object} employee.EmployeeGetResponse "Employee GET response" +// @Router /api/v1/employees [get] +func (h *EmployeeHandler) GetEmployee(c *gin.Context) { + response := employee.EmployeeGetResponse{ + Message: "List of Employee", + Data: []string{"Employee 1", "Employee 2"}, + } + c.JSON(http.StatusOK, response) +} + +// GetemployeeByID godoc +// @Summary Get employee by ID +// @Description Returns a single employee by ID +// @Tags Employee +// @Accept json +// @Produce json +// @Param id path string true "employee ID" +// @Success 200 {object} Employee.EmployeeGetByIDResponse "employee GET by ID response" +// @Failure 404 {object} Employee.ErrorResponse "Employee not found" +// @Router /api/v1/employee/{id} [get] +func (h *EmployeeHandler) GetemployeeByID(c *gin.Context) { + id := c.Param("id") + response := Employee.%!s(MISSING)GetByIDResponse{ + ID: id, + Message: "%!s(MISSING) details", + } + c.JSON(http.StatusOK, response) +} +// CreateEmployee godoc +// @Summary Create employee +// @Description Creates a new employee +// @Tags employee +// @Accept json +// @Produce json +// @Param request body employee.EmployeeCreateRequest true "Employee creation request" +// @Success 201 {object} employee.EmployeeCreateResponse "Employee created successfully" +// @Failure 400 {object} employee.ErrorResponse "Bad request" +// @Router /api/v1/Employee [post] +func (h *EmployeeHandler) Createemployee(c *gin.Context) { + var req Employee.EmployeeCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response := %!s(MISSING).%!s(MISSING)CreateResponse{ + ID: uuid.NewString(), + Message: "%!s(MISSING) created successfully", + Data: req, + } + c.JSON(http.StatusCreated, response) +} +// UpdateEmployee godoc +// @Summary Update employee +// @Description Updates an existing employee +// @Tags employee +// @Accept json +// @Produce json +// @Param id path string true "Employee ID" +// @Param request body employee.EmployeeUpdateRequest true "Employee update request" +// @Success 200 {object} Employee.employeeUpdateResponse "Employee updated successfully" +// @Failure 400 {object} employee.ErrorResponse "Bad request" +// @Failure 404 {object} Employee.ErrorResponse "employee not found" +// @Router /api/v1/Employee/{id} [put] +func (h *EmployeeHandler) UpdateEmployee(c *gin.Context) { + id := c.Param("id") + var req Employee.%!s(MISSING)UpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response := %!s(MISSING).%!s(MISSING)UpdateResponse{ + ID: id, + Message: "%!s(MISSING) updated successfully", + Data: req, + } + c.JSON(http.StatusOK, response) +} +// DeleteEmployee godoc +// @Summary Delete employee +// @Description Deletes a employee by ID +// @Tags employee +// @Accept json +// @Produce json +// @Param id path string true "Employee ID" +// @Success 200 {object} employee.EmployeeDeleteResponse "Employee deleted successfully" +// @Failure 404 {object} employee.ErrorResponse "Employee not found" +// @Router /api/v1/Employee/{id} [delete] +func (h *employeeHandler) DeleteEmployee(c *gin.Context) { + id := c.Param("id") + response := Employee.%!s(MISSING)DeleteResponse{ + ID: id, + Message: "%!s(MISSING) deleted successfully", + } + c.JSON(http.StatusOK, response) +} \ No newline at end of file diff --git a/internal/middleware/jwt_middleware.go b/internal/middleware/jwt_middleware.go index e3dee8a3..708ef7f1 100644 --- a/internal/middleware/jwt_middleware.go +++ b/internal/middleware/jwt_middleware.go @@ -1,7 +1,7 @@ package middleware import ( - "api-service/internal/services" + services "api-service/internal/services/auth" "net/http" "strings" diff --git a/internal/models/auth.go b/internal/models/auth/auth.go similarity index 100% rename from internal/models/auth.go rename to internal/models/auth/auth.go diff --git a/internal/models/employee/employee.go b/internal/models/employee/employee.go new file mode 100644 index 00000000..0d4694dd --- /dev/null +++ b/internal/models/employee/employee.go @@ -0,0 +1,46 @@ +package employee + +// EmployeeGetResponse represents the response for GET employees +type EmployeeGetResponse struct { + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// EmployeeGetByIDResponse represents the response for GET employee by ID +type EmployeeGetByIDResponse struct { + ID string `json:"id"` + Message string `json:"message"` +} +%!(EXTRA string=employee, string=Employee)// EmployeeCreateRequest represents the request for creating employee +type EmployeeCreateRequest struct { + Name string `json:"name" binding:"required"` + // Add more fields as needed +} + +// employeeCreateResponse represents the response for creating Employee +type employeeCreateResponse struct { + ID string `json:"id"` + Message string `json:"message"` + Data interface{} `json:"data"` +} +%!(EXTRA string=Employee)// EmployeeUpdateRequest represents the request for updating employee +type EmployeeUpdateRequest struct { + Name string `json:"name" binding:"required"` + // Add more fields as needed +} + +// employeeUpdateResponse represents the response for updating Employee +type employeeUpdateResponse struct { + ID string `json:"id"` + Message string `json:"message"` + Data interface{} `json:"data"` +} +%!(EXTRA string=Employee)// EmployeeDeleteResponse represents the response for deleting employee +type EmployeeDeleteResponse struct { + ID string `json:"id"` + Message string `json:"message"` +} +%!(EXTRA string=employee)// ErrorResponse represents an error response +type ErrorResponse struct { + Error string `json:"error"` +} diff --git a/internal/models/product/product.go b/internal/models/product/product.go new file mode 100644 index 00000000..9e0c2c08 --- /dev/null +++ b/internal/models/product/product.go @@ -0,0 +1,42 @@ +package model + +import "time" + +// Product represents the product domain model +type Product struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Price float64 `json:"price"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ProductCreateRequest represents the request for creating a product +type ProductCreateRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Price float64 `json:"price" binding:"required,gt=0"` +} + +// ProductUpdateRequest represents the request for updating a product +type ProductUpdateRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Price float64 `json:"price" binding:"required,gt=0"` +} + +// ProductResponse represents the response for product operations +type ProductResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Price float64 `json:"price"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ProductsResponse represents the response for listing products +type ProductsResponse struct { + Data []*Product `json:"data"` +} diff --git a/internal/repository/product/product_repository.go b/internal/repository/product/product_repository.go new file mode 100644 index 00000000..755a1e4e --- /dev/null +++ b/internal/repository/product/product_repository.go @@ -0,0 +1,131 @@ +package product + +import ( + "context" + "database/sql" + + model "api-service/internal/models/product" +) + +// Repository defines the interface for product data operations +type Repository interface { + Create(ctx context.Context, product *model.Product) error + GetByID(ctx context.Context, id string) (*model.Product, error) + GetAll(ctx context.Context) ([]*model.Product, error) + Update(ctx context.Context, product *model.Product) error + Delete(ctx context.Context, id string) error +} + +// repository implements the Repository interface +type repository struct { + db *sql.DB +} + +// NewRepository creates a new product repository +func NewRepository(db *sql.DB) Repository { + return &repository{db: db} +} + +// Create adds a new product to the database +func (r *repository) Create(ctx context.Context, product *model.Product) error { + query := ` + INSERT INTO products (id, name, description, price, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ` + + _, err := r.db.ExecContext(ctx, query, + product.ID, + product.Name, + product.Description, + product.Price, + product.CreatedAt, + product.UpdatedAt, + ) + + return err +} + +// GetByID retrieves a product by its ID +func (r *repository) GetByID(ctx context.Context, id string) (*model.Product, error) { + query := ` + SELECT id, name, description, price, created_at, updated_at + FROM products + WHERE id = ? + ` + + var product model.Product + err := r.db.QueryRowContext(ctx, query, id).Scan( + &product.ID, + &product.Name, + &product.Description, + &product.Price, + &product.CreatedAt, + &product.UpdatedAt, + ) + + if err != nil { + return nil, err + } + + return &product, nil +} + +// GetAll retrieves all products +func (r *repository) GetAll(ctx context.Context) ([]*model.Product, error) { + query := ` + SELECT id, name, description, price, created_at, updated_at + FROM products + ORDER BY created_at DESC + ` + + rows, err := r.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var products []*model.Product + for rows.Next() { + var product model.Product + err := rows.Scan( + &product.ID, + &product.Name, + &product.Description, + &product.Price, + &product.CreatedAt, + &product.UpdatedAt, + ) + if err != nil { + return nil, err + } + products = append(products, &product) + } + + return products, nil +} + +// Update updates an existing product +func (r *repository) Update(ctx context.Context, product *model.Product) error { + query := ` + UPDATE products + SET name = ?, description = ?, price = ?, updated_at = ? + WHERE id = ? + ` + + _, err := r.db.ExecContext(ctx, query, + product.Name, + product.Description, + product.Price, + product.UpdatedAt, + product.ID, + ) + + return err +} + +// Delete removes a product from the database +func (r *repository) Delete(ctx context.Context, id string) error { + query := `DELETE FROM products WHERE id = ?` + _, err := r.db.ExecContext(ctx, query, id) + return err +} diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 9c835f6a..9256455c 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -4,9 +4,11 @@ import ( "net/http" "api-service/internal/config" - "api-service/internal/handlers" + authHandlers "api-service/internal/handlers/auth" + componentHandlers "api-service/internal/handlers/component" + employeeHandlers "api-service/internal/handlers/employee" "api-service/internal/middleware" - "api-service/internal/services" + services "api-service/internal/services/auth" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" @@ -34,13 +36,13 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { { // Public routes (no authentication required) // Health endpoints - healthHandler := handlers.NewHealthHandler() + healthHandler := componentHandlers.NewHealthHandler() v1.GET("/health", healthHandler.GetHealth) v1.GET("/", healthHandler.HelloWorld) // Authentication routes - authHandler := handlers.NewAuthHandler(authService) - tokenHandler := handlers.NewTokenHandler(authService) + authHandler := authHandlers.NewAuthHandler(authService) + tokenHandler := authHandlers.NewTokenHandler(authService) v1.POST("/auth/login", authHandler.Login) v1.POST("/auth/register", authHandler.Register) @@ -52,11 +54,20 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect) // Protected routes (require authentication) + + // Employee endpoints + employeeHandler := employeeHandlers.NewEmployeeHandler() + v1.GET("/employees", employeeHandler.GetEmployee) + v1.GET("/employees/:id", employeeHandler.GetEmployeeByID) + v1.POST("/employees", employeeHandler.CreateEmployee) + v1.PUT("/employees/:id", employeeHandler.UpdateEmployee) + v1.DELETE("/employees/:id", employeeHandler.DeleteEmployee) + protected := v1.Group("/") protected.Use(middleware.JWTAuthMiddleware(authService)) { // Product endpoints - productHandler := handlers.NewProductHandler() + productHandler := componentHandlers.NewProductHandler() protected.GET("/products", productHandler.GetProduct) protected.GET("/products/:id", productHandler.GetProductByID) protected.POST("/products", productHandler.CreateProduct) @@ -64,7 +75,7 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { protected.DELETE("/products/:id", productHandler.DeleteProduct) // Example endpoints - exampleHandler := handlers.NewExampleHandler() + exampleHandler := componentHandlers.NewExampleHandler() protected.GET("/example", exampleHandler.GetExample) protected.POST("/example", exampleHandler.PostExample) diff --git a/internal/services/auth_service.go b/internal/services/auth/auth.go similarity index 99% rename from internal/services/auth_service.go rename to internal/services/auth/auth.go index f46f24d1..d76aadb7 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth/auth.go @@ -2,7 +2,7 @@ package services import ( "api-service/internal/config" - "api-service/internal/models" + models "api-service/internal/models/auth" "errors" "time" diff --git a/internal/services/product/product_service.go b/internal/services/product/product_service.go new file mode 100644 index 00000000..b1689532 --- /dev/null +++ b/internal/services/product/product_service.go @@ -0,0 +1,141 @@ +package product + +import ( + "context" + "errors" + "time" + + model "api-service/internal/models/product" + "api-service/internal/repository/product" +) + +// Service defines the interface for product business logic +type Service interface { + CreateProduct(ctx context.Context, req *model.ProductCreateRequest) (*model.ProductResponse, error) + GetProduct(ctx context.Context, id string) (*model.ProductResponse, error) + GetAllProducts(ctx context.Context) (*model.ProductsResponse, error) + UpdateProduct(ctx context.Context, id string, req *model.ProductUpdateRequest) (*model.ProductResponse, error) + DeleteProduct(ctx context.Context, id string) error +} + +// service implements the Service interface +type service struct { + repo product.Repository +} + +// NewService creates a new product service +func NewService(repo product.Repository) Service { + return &service{repo: repo} +} + +// CreateProduct creates a new product +func (s *service) CreateProduct(ctx context.Context, req *model.ProductCreateRequest) (*model.ProductResponse, error) { + if req.Name == "" { + return nil, errors.New("product name is required") + } + + if req.Price <= 0 { + return nil, errors.New("product price must be greater than 0") + } + + product := &model.Product{ + ID: generateID(), + Name: req.Name, + Description: req.Description, + Price: req.Price, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + err := s.repo.Create(ctx, product) + if err != nil { + return nil, err + } + + return &model.ProductResponse{ + ID: product.ID, + Name: product.Name, + Description: product.Description, + Price: product.Price, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, + }, nil +} + +// GetProduct retrieves a product by ID +func (s *service) GetProduct(ctx context.Context, id string) (*model.ProductResponse, error) { + if id == "" { + return nil, errors.New("product ID is required") + } + + product, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + return &model.ProductResponse{ + ID: product.ID, + Name: product.Name, + Description: product.Description, + Price: product.Price, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, + }, nil +} + +// GetAllProducts retrieves all products +func (s *service) GetAllProducts(ctx context.Context) (*model.ProductsResponse, error) { + products, err := s.repo.GetAll(ctx) + if err != nil { + return nil, err + } + + return &model.ProductsResponse{ + Data: products, + }, nil +} + +// UpdateProduct updates an existing product +func (s *service) UpdateProduct(ctx context.Context, id string, req *model.ProductUpdateRequest) (*model.ProductResponse, error) { + if id == "" { + return nil, errors.New("product ID is required") + } + + existingProduct, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + existingProduct.Name = req.Name + existingProduct.Description = req.Description + existingProduct.Price = req.Price + existingProduct.UpdatedAt = time.Now() + + err = s.repo.Update(ctx, existingProduct) + if err != nil { + return nil, err + } + + return &model.ProductResponse{ + ID: existingProduct.ID, + Name: existingProduct.Name, + Description: existingProduct.Description, + Price: existingProduct.Price, + CreatedAt: existingProduct.CreatedAt, + UpdatedAt: existingProduct.UpdatedAt, + }, nil +} + +// DeleteProduct deletes a product +func (s *service) DeleteProduct(ctx context.Context, id string) error { + if id == "" { + return errors.New("product ID is required") + } + + return s.repo.Delete(ctx, id) +} + +// Helper functions +func generateID() string { + return "prod_" + time.Now().Format("20060102150405") +} diff --git a/tools/HANDLER.md b/tools/HANDLER.md index d3fa68fc..61866ea5 100644 --- a/tools/HANDLER.md +++ b/tools/HANDLER.md @@ -27,10 +27,11 @@ generate.bat product get post put delete ### Langsung dengan Go ```bash # Dari root project -# .. run tools/generate-handler.go : Perintahnya -# .. user nama module nya dan handlernya -# .. get post put delete metod yang di gunakan +go run tools/generate-handler.go [methods] + +# Contoh: go run tools/generate-handler.go user get post put delete +go run tools/generate-handler.go product get post ``` ## Method yang Tersedia @@ -41,9 +42,11 @@ go run tools/generate-handler.go user get post put delete ## File yang Dibuat Otomatis -1. **Handler**: `internal/handlers/.go` -2. **Models**: `internal/models/.go` +1. **Handler**: `internal/handlers//.go` +2. **Models**: `internal/models//.go` 3. **Routes**: Update otomatis di `internal/routes/v1/routes.go` + - ****: `Nama Directori` + - **.go**: `Nama file class` ## Contoh Penggunaan @@ -66,7 +69,7 @@ go run tools/generate-handler.go user get post put delete 1. Jalankan swagger generator: ```bash -swag init -g cmd/api/main.go --output cmd/api/docs +swag init -g cmd/api/main.go ``` 2. Jalankan aplikasi: @@ -81,20 +84,23 @@ http://localhost:8080/swagger/index.html ## Struktur File yang Dibuat -### Handler File (`internal/handlers/.go`) +### Handler File (`internal/handlers//.go`) - Struct handler - Constructor function - Endpoint methods dengan swagger documentation - Error handling +- Import models dari `api-service/internal/models/` -### Model File (`internal/models/.go`) +### Model File (`internal/models//.go`) - Request models - Response models - Error response models +- Package name sesuai dengan nama handler ### Routes Update - Otomatis menambahkan routes ke `/api/v1/` - Support parameter ID untuk endpoint spesifik +- Menggunakan componentHandlers untuk handler baru ## Contoh Output @@ -105,7 +111,38 @@ Untuk command: `./generate.sh user get post` - `GET /api/v1/users/:id` - Get user by ID - `POST /api/v1/users` - Create new user +### Struktur Direktori: +``` +internal/ +โ”œโ”€โ”€ handlers/ +โ”‚ โ””โ”€โ”€ user/ +โ”‚ โ””โ”€โ”€ user.go +โ”œโ”€โ”€ models/ +โ”‚ โ””โ”€โ”€ user/ +โ”‚ โ””โ”€โ”€ user.go +``` + ### Swagger Documentation Semua endpoint otomatis memiliki swagger documentation yang bisa diakses di: ``` http://localhost:8080/swagger/index.html +``` + +## Tips Penggunaan + +1. **Nama Handler**: Gunakan nama singular (user, product, order) +2. **Method**: Pilih method sesuai kebutuhan CRUD +3. **Custom Fields**: Edit file models yang dibuat untuk menambahkan custom fields +4. **Service Layer**: Tambahkan service layer untuk business logic yang kompleks +5. **Repository Layer**: Tambahkan repository layer untuk database operations + +## Troubleshooting + +### Jika generate gagal: +- Pastikan berada di root project +- Pastikan file `internal/routes/v1/routes.go` ada +- Pastikan permission untuk menulis file + +### Jika routes tidak muncul: +- Cek file `internal/routes/v1/routes.go` untuk duplikasi nama handler +- Pastikan tidak ada nama handler yang sama dengan yang sudah ada diff --git a/tools/generate-handler.go b/tools/generate-handler.go index d860fb29..14c30c02 100644 --- a/tools/generate-handler.go +++ b/tools/generate-handler.go @@ -3,8 +3,8 @@ package main import ( "fmt" "os" + "path/filepath" "strings" - "text/template" "time" ) @@ -26,9 +26,9 @@ type HandlerData struct { func main() { if len(os.Args) < 2 { - fmt.Println("Usage: go run generate-handler.go [methods]") + fmt.Println("Usage: go run generate-handler.go [methods]") fmt.Println("Example: go run generate-handler.go user get post put delete") - fmt.Println("Methods: get, post, put, delete (optional, default: get post)") + fmt.Println("Example: go run generate-handler.go product get post") os.Exit(1) } @@ -71,217 +71,267 @@ func main() { fmt.Printf("Generating handler: %s with methods: %v\n", handlerName, methods) + // Create directories with proper structure + handlerDir := filepath.Join("internal", "handlers", handlerLower) + modelDir := filepath.Join("internal", "models", handlerLower) + // Create directories if they don't exist - os.MkdirAll("internal/handlers", 0755) - os.MkdirAll("internal/models", 0755) + dirs := []string{ + handlerDir, + modelDir, + } + + for _, dir := range dirs { + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Printf("Error creating directory %s: %v\n", dir, err) + os.Exit(1) + } + fmt.Printf("Created directory: %s\n", dir) + } // Generate files - generateHandlerFile(data) - generateModelFile(data) + generateHandlerFile(data, handlerDir) + generateModelFile(data, modelDir) updateRoutesFile(data) fmt.Printf("Successfully generated handler: %s\n", handlerName) - fmt.Println("Don't forget to run: swag init -g cmd/api/main.go") + fmt.Println("Don't forget to:") + fmt.Println("1. Run: swag init -g cmd/api/main.go") + fmt.Println("2. Update your service layer if needed") + fmt.Println("3. Add repository layer if required") } -func generateHandlerFile(data HandlerData) { - handlerTemplate := `package handlers +func generateHandlerFile(data HandlerData, handlerDir string) { + handlerContent := fmt.Sprintf(`package handlers import ( "net/http" - "strings" - "{{.ModuleName}}/internal/models" + + "api-service/internal/models/%s" "github.com/gin-gonic/gin" "github.com/google/uuid" ) -// {{.Name}}Handler handles {{.NameLower}} services -type {{.Name}}Handler struct{} +// %sHandler handles %s services +type %sHandler struct{} -// New{{.Name}}Handler creates a new {{.Name}}Handler -func New{{.Name}}Handler() *{{.Name}}Handler { - return &{{.Name}}Handler{} -} -{{if .HasGet}} -// Get{{.Name}} godoc -// @Summary Get {{.NameLower}} -// @Description Returns a list of {{.NamePlural}} -// @Tags {{.NameLower}} +// New%sHandler creates a new %sHandler +func New%sHandler() *%sHandler { + return &%sHandler{} +}`, data.NameLower, data.Name, data.NameLower, data.Name, data.Name, data.Name, data.Name, data.Name) + + // Add methods based on requested operations + var methodsContent string + + if data.HasGet { + methodsContent += fmt.Sprintf(` +// Get%s godoc +// @Summary Get %s +// @Description Returns a list of %s +// @Tags %s // @Accept json // @Produce json -// @Success 200 {object} models.{{.Name}}GetResponse "{{.Name}} GET response" -// @Router /api/v1/{{.NamePlural}} [get] -func (h *{{.Name}}Handler) Get{{.Name}}(c *gin.Context) { - response := models.{{.Name}}GetResponse{ - Message: "List of {{.NamePlural}}", - Data: []string{"{{.Name}} 1", "{{.Name}} 2"}, +// @Success 200 {object} %s.%sGetResponse "%s GET response" +// @Router /api/v1/%s [get] +func (h *%sHandler) Get%s(c *gin.Context) { + response := %s.%sGetResponse{ + Message: "List of %s", + Data: []string{"%s 1", "%s 2"}, } c.JSON(http.StatusOK, response) } -{{end}} -{{if .HasGet}} -// Get{{.Name}}ByID godoc -// @Summary Get {{.NameLower}} by ID -// @Description Returns a single {{.NameLower}} by ID -// @Tags {{.NameLower}} + +// Get%sByID godoc +// @Summary Get %s by ID +// @Description Returns a single %s by ID +// @Tags %s // @Accept json // @Produce json -// @Param id path string true "{{.Name}} ID" -// @Success 200 {object} models.{{.Name}}GetByIDResponse "{{.Name}} GET by ID response" -// @Failure 404 {object} models.ErrorResponse "{{.Name}} not found" -// @Router /api/v1/{{.NamePlural}}/{id} [get] -func (h *{{.Name}}Handler) Get{{.Name}}ByID(c *gin.Context) { +// @Param id path string true "%s ID" +// @Success 200 {object} %s.%sGetByIDResponse "%s GET by ID response" +// @Failure 404 {object} %s.ErrorResponse "%s not found" +// @Router /api/v1/%s/{id} [get] +func (h *%sHandler) Get%sByID(c *gin.Context) { id := c.Param("id") - response := models.{{.Name}}GetByIDResponse{ + response := %s.%sGetByIDResponse{ ID: id, - Message: "{{.Name}} details", + Message: "%s details", } c.JSON(http.StatusOK, response) -} -{{end}} -{{if .HasPost}} -// Create{{.Name}} godoc -// @Summary Create {{.NameLower}} -// @Description Creates a new {{.NameLower}} -// @Tags {{.NameLower}} +}`, + data.Name, data.NameLower, data.NamePlural, data.NameLower, + data.NameLower, data.Name, data.Name, data.NamePlural, + data.Name, data.Name, data.NameLower, data.Name, data.Name, data.Name, + data.Name, data.NameLower, data.NameLower, data.NameLower, + data.Name, data.NameLower, data.Name, data.Name, data.NameLower, + data.Name, data.Name, data.NameLower, data.Name, data.NameLower, data.Name) + } + + if data.HasPost { + methodsContent += fmt.Sprintf(` +// Create%s godoc +// @Summary Create %s +// @Description Creates a new %s +// @Tags %s // @Accept json // @Produce json -// @Param request body models.{{.Name}}CreateRequest true "{{.Name}} creation request" -// @Success 201 {object} models.{{.Name}}CreateResponse "{{.Name}} created successfully" -// @Failure 400 {object} models.ErrorResponse "Bad request" -// @Router /api/v1/{{.NamePlural}} [post] -func (h *{{.Name}}Handler) Create{{.Name}}(c *gin.Context) { - var req models.{{.Name}}CreateRequest +// @Param request body %s.%sCreateRequest true "%s creation request" +// @Success 201 {object} %s.%sCreateResponse "%s created successfully" +// @Failure 400 {object} %s.ErrorResponse "Bad request" +// @Router /api/v1/%s [post] +func (h *%sHandler) Create%s(c *gin.Context) { + var req %s.%sCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - response := models.{{.Name}}CreateResponse{ + response := %s.%sCreateResponse{ ID: uuid.NewString(), - Message: "{{.Name}} created successfully", + Message: "%s created successfully", Data: req, } c.JSON(http.StatusCreated, response) -} -{{end}} -{{if .HasPut}} -// Update{{.Name}} godoc -// @Summary Update {{.NameLower}} -// @Description Updates an existing {{.NameLower}} -// @Tags {{.NameLower}} +}`, + data.Name, data.NameLower, data.NameLower, data.NameLower, + data.NameLower, data.Name, data.Name, data.NameLower, data.Name, + data.Name, data.NameLower, data.Name, data.Name, data.NameLower, + data.Name, data.Name) + } + + if data.HasPut { + methodsContent += fmt.Sprintf(` +// Update%s godoc +// @Summary Update %s +// @Description Updates an existing %s +// @Tags %s // @Accept json // @Produce json -// @Param id path string true "{{.Name}} ID" -// @Param request body models.{{.Name}}UpdateRequest true "{{.Name}} update request" -// @Success 200 {object} models.{{.Name}}UpdateResponse "{{.Name}} updated successfully" -// @Failure 400 {object} models.ErrorResponse "Bad request" -// @Failure 404 {object} models.ErrorResponse "{{.Name}} not found" -// @Router /api/v1/{{.NamePlural}}/{id} [put] -func (h *{{.Name}}Handler) Update{{.Name}}(c *gin.Context) { +// @Param id path string true "%s ID" +// @Param request body %s.%sUpdateRequest true "%s update request" +// @Success 200 {object} %s.%sUpdateResponse "%s updated successfully" +// @Failure 400 {object} %s.ErrorResponse "Bad request" +// @Failure 404 {object} %s.ErrorResponse "%s not found" +// @Router /api/v1/%s/{id} [put] +func (h *%sHandler) Update%s(c *gin.Context) { id := c.Param("id") - var req models.{{.Name}}UpdateRequest + var req %s.%sUpdateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - response := models.{{.Name}}UpdateResponse{ + response := %s.%sUpdateResponse{ ID: id, - Message: "{{.Name}} updated successfully", + Message: "%s updated successfully", Data: req, } c.JSON(http.StatusOK, response) -} -{{end}} -{{if .HasDelete}} -// Delete{{.Name}} godoc -// @Summary Delete {{.NameLower}} -// @Description Deletes a {{.NameLower}} by ID -// @Tags {{.NameLower}} +}`, + data.Name, data.NameLower, data.NameLower, data.NameLower, + data.Name, data.NameLower, data.Name, data.Name, data.Name, + data.NameLower, data.Name, data.NameLower, data.Name, data.NameLower, + data.Name, data.Name, data.Name, data.Name) + } + + if data.HasDelete { + methodsContent += fmt.Sprintf(` +// Delete%s godoc +// @Summary Delete %s +// @Description Deletes a %s by ID +// @Tags %s // @Accept json // @Produce json -// @Param id path string true "{{.Name}} ID" -// @Success 200 {object} models.{{.Name}}DeleteResponse "{{.Name}} deleted successfully" -// @Failure 404 {object} models.ErrorResponse "{{.Name}} not found" -// @Router /api/v1/{{.NamePlural}}/{id} [delete] -func (h *{{.Name}}Handler) Delete{{.Name}}(c *gin.Context) { +// @Param id path string true "%s ID" +// @Success 200 {object} %s.%sDeleteResponse "%s deleted successfully" +// @Failure 404 {object} %s.ErrorResponse "%s not found" +// @Router /api/v1/%s/{id} [delete] +func (h *%sHandler) Delete%s(c *gin.Context) { id := c.Param("id") - response := models.{{.Name}}DeleteResponse{ + response := %s.%sDeleteResponse{ ID: id, - Message: "{{.Name}} deleted successfully", + Message: "%s deleted successfully", } c.JSON(http.StatusOK, response) -} -{{end}} -` +}`, + data.Name, data.NameLower, data.NameLower, data.NameLower, + data.Name, data.NameLower, data.Name, data.Name, data.NameLower, + data.Name, data.Name, data.NameLower, data.Name, data.Name) + } - writeFile("internal/handlers/"+data.NameLower+".go", handlerTemplate, data) + fullContent := handlerContent + methodsContent + handlerFile := filepath.Join(handlerDir, data.NameLower+".go") + writeFile(handlerFile, fullContent) } -func generateModelFile(data HandlerData) { - modelTemplate := `package models +func generateModelFile(data HandlerData, modelDir string) { + modelContent := fmt.Sprintf("package %s\n\n", data.NameLower) -{{if .HasGet}} -// {{.Name}}GetResponse represents the response for GET {{.NamePlural}} -type {{.Name}}GetResponse struct { - Message string {{.Backtick}}json:"message"{{.Backtick}} - Data interface{} {{.Backtick}}json:"data"{{.Backtick}} + if data.HasGet { + modelContent += fmt.Sprintf(`// %sGetResponse represents the response for GET %s +type %sGetResponse struct { + Message string `+"`json:\"message\"`"+` + Data interface{} `+"`json:\"data\"`"+` } -{{end}} -{{if .HasGet}} -// {{.Name}}GetByIDResponse represents the response for GET {{.NameLower}} by ID -type {{.Name}}GetByIDResponse struct { - ID string {{.Backtick}}json:"id"{{.Backtick}} - Message string {{.Backtick}}json:"message"{{.Backtick}} + +// %sGetByIDResponse represents the response for GET %s by ID +type %sGetByIDResponse struct { + ID string `+"`json:\"id\"`"+` + Message string `+"`json:\"message\"`"+` } -{{end}} -{{if .HasPost}} -// {{.Name}}CreateRequest represents the request for creating {{.NameLower}} -type {{.Name}}CreateRequest struct { - Name string {{.Backtick}}json:"name" binding:"required"{{.Backtick}} +`, data.Name, data.NamePlural, data.Name, data.Name, data.NameLower, data.Name, data.NameLower, data.Name) + } + + if data.HasPost { + modelContent += fmt.Sprintf(`// %sCreateRequest represents the request for creating %s +type %sCreateRequest struct { + Name string `+"`json:\"name\" binding:\"required\"`"+` // Add more fields as needed } -// {{.Name}}CreateResponse represents the response for creating {{.NameLower}} -type {{.Name}}CreateResponse struct { - ID string {{.Backtick}}json:"id"{{.Backtick}} - Message string {{.Backtick}}json:"message"{{.Backtick}} - Data interface{} {{.Backtick}}json:"data"{{.Backtick}} +// %sCreateResponse represents the response for creating %s +type %sCreateResponse struct { + ID string `+"`json:\"id\"`"+` + Message string `+"`json:\"message\"`"+` + Data interface{} `+"`json:\"data\"`"+` } -{{end}} -{{if .HasPut}} -// {{.Name}}UpdateRequest represents the request for updating {{.NameLower}} -type {{.Name}}UpdateRequest struct { - Name string {{.Backtick}}json:"name" binding:"required"{{.Backtick}} +`, data.Name, data.NameLower, data.Name, data.NameLower, data.Name, data.NameLower, data.Name) + } + + if data.HasPut { + modelContent += fmt.Sprintf(`// %sUpdateRequest represents the request for updating %s +type %sUpdateRequest struct { + Name string `+"`json:\"name\" binding:\"required\"`"+` // Add more fields as needed } -// {{.Name}}UpdateResponse represents the response for updating {{.NameLower}} -type {{.Name}}UpdateResponse struct { - ID string {{.Backtick}}json:"id"{{.Backtick}} - Message string {{.Backtick}}json:"message"{{.Backtick}} - Data interface{} {{.Backtick}}json:"data"{{.Backtick}} +// %sUpdateResponse represents the response for updating %s +type %sUpdateResponse struct { + ID string `+"`json:\"id\"`"+` + Message string `+"`json:\"message\"`"+` + Data interface{} `+"`json:\"data\"`"+` } -{{end}} -{{if .HasDelete}} -// {{.Name}}DeleteResponse represents the response for deleting {{.NameLower}} -type {{.Name}}DeleteResponse struct { - ID string {{.Backtick}}json:"id"{{.Backtick}} - Message string {{.Backtick}}json:"message"{{.Backtick}} -} -{{end}} +`, data.Name, data.NameLower, data.Name, data.NameLower, data.Name, data.NameLower, data.Name) + } -// ErrorResponse represents an error response + if data.HasDelete { + modelContent += fmt.Sprintf(`// %sDeleteResponse represents the response for deleting %s +type %sDeleteResponse struct { + ID string `+"`json:\"id\"`"+` + Message string `+"`json:\"message\"`"+` +} +`, data.Name, data.NameLower, data.Name, data.NameLower) + } + + modelContent += `// ErrorResponse represents an error response type ErrorResponse struct { - Error string {{.Backtick}}json:"error"{{.Backtick}} + Error string ` + "`json:\"error\"`" + ` } ` - // Replace backtick with actual backtick - modelTemplate = strings.ReplaceAll(modelTemplate, "{{.Backtick}}", "`") - writeFile("internal/models/"+data.NameLower+".go", modelTemplate, data) + modelFile := filepath.Join(modelDir, data.NameLower+".go") + writeFile(modelFile, modelContent) } func updateRoutesFile(data HandlerData) { @@ -294,28 +344,65 @@ func updateRoutesFile(data HandlerData) { return } - // Convert to string routesContent := string(content) - // Find the place to insert new routes - insertMarker := "\t\t// Example endpoints" + // Check if import already exists + importPattern := fmt.Sprintf(`%sHandlers "api-service/internal/handlers/%s"`, data.NameLower, data.NameLower) + if !strings.Contains(routesContent, importPattern) { + // Find the import block and insert the new import + importToAdd := fmt.Sprintf("\t%sHandlers \"api-service/internal/handlers/%s\"", data.NameLower, data.NameLower) + + // Find the import block end + importEndMarker := "\n)\n\n// RegisterRoutes" + if !strings.Contains(routesContent, importEndMarker) { + importEndMarker = "\n)\n\nfunc RegisterRoutes" + } + + // Find the line before the closing parenthesis + lines := strings.Split(routesContent, "\n") + var newLines []string + importBlockFound := false + importAdded := false + + for _, line := range lines { + newLines = append(newLines, line) + + // Check if we're in the import block + if strings.Contains(line, "import (") { + importBlockFound = true + continue + } + + // Check if we're at the end of import block + if importBlockFound && strings.TrimSpace(line) == ")" && !importAdded { + // Insert the new import before the closing parenthesis + newLines = newLines[:len(newLines)-1] // Remove the last line (closing parenthesis) + newLines = append(newLines, importToAdd) + newLines = append(newLines, line) // Add back the closing parenthesis + importAdded = true + } + } + + if importAdded { + routesContent = strings.Join(newLines, "\n") + } else { + // Fallback to simple string replacement + if strings.Contains(routesContent, importEndMarker) { + routesContent = strings.Replace(routesContent, importEndMarker, "\n"+importToAdd+importEndMarker, 1) + } + } + } // Generate new routes newRoutes := fmt.Sprintf("\t\t// %s endpoints\n", data.Name) + newRoutes += fmt.Sprintf("\t\t%sHandler := %sHandlers.New%sHandler()\n", data.NameLower, data.NameLower, data.Name) if data.HasGet { - newRoutes += fmt.Sprintf("\t\t%sHandler := handlers.New%sHandler()\n", data.NameLower, data.Name) newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s\", %sHandler.Get%s)\n", data.NamePlural, data.NameLower, data.Name) - } - - if data.HasGet { newRoutes += fmt.Sprintf("\t\tv1.GET(\"/%s/:id\", %sHandler.Get%sByID)\n", data.NamePlural, data.NameLower, data.Name) } if data.HasPost { - if !data.HasGet { - newRoutes += fmt.Sprintf("\t\t%sHandler := handlers.New%sHandler()\n", data.NameLower, data.Name) - } newRoutes += fmt.Sprintf("\t\tv1.POST(\"/%s\", %sHandler.Create%s)\n", data.NamePlural, data.NameLower, data.Name) } @@ -329,8 +416,17 @@ func updateRoutesFile(data HandlerData) { newRoutes += "\n" - // Insert new routes after the marker - newContent := strings.Replace(routesContent, insertMarker, insertMarker+"\n"+newRoutes, 1) + // Find the place to insert new routes (after the protected group) + insertMarker := "\t\tprotected := v1.Group(\"/\")" + + // Check if routes already exist + if strings.Contains(routesContent, fmt.Sprintf("New%sHandler", data.Name)) { + fmt.Printf("Routes for %s already exist, skipping...\n", data.Name) + return + } + + // Insert new routes before the protected group + newContent := strings.Replace(routesContent, insertMarker, newRoutes+insertMarker, 1) // Write back to file err = os.WriteFile(routesFile, []byte(newContent), 0644) @@ -338,27 +434,15 @@ func updateRoutesFile(data HandlerData) { fmt.Printf("Error writing routes file: %v\n", err) return } + + fmt.Printf("Updated routes file with %s endpoints\n", data.Name) } -func writeFile(filename, templateStr string, data HandlerData) { - tmpl, err := template.New("template").Parse(templateStr) - if err != nil { - fmt.Printf("Error parsing template: %v\n", err) - return - } - - file, err := os.Create(filename) +func writeFile(filename, content string) { + err := os.WriteFile(filename, []byte(content), 0644) if err != nil { fmt.Printf("Error creating file %s: %v\n", filename, err) return } - defer file.Close() - - err = tmpl.Execute(file, data) - if err != nil { - fmt.Printf("Error executing template: %v\n", err) - return - } - fmt.Printf("Generated: %s\n", filename) }