diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md new file mode 100644 index 00000000..fc308601 --- /dev/null +++ b/AUTHENTICATION.md @@ -0,0 +1,141 @@ +# JWT Authentication API Documentation + +This document describes how to use the JWT authentication system implemented in this API service. + +## Overview + +The API provides JWT-based authentication with the following features: +- User login with username/password +- JWT token generation +- Token validation middleware +- Protected routes +- User registration + +## Endpoints + +### Public Endpoints (No Authentication Required) + +#### POST /api/v1/auth/login +Login with username and password to receive a JWT token. + +**Request Body:** +```json +{ + "username": "admin", + "password": "password" +} +``` + +**Response:** +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 3600 +} +``` + +#### POST /api/v1/auth/register +Register a new user account. + +**Request Body:** +```json +{ + "username": "newuser", + "email": "user@example.com", + "password": "securepassword", + "role": "user" +} +``` + +**Response:** +```json +{ + "message": "user registered successfully" +} +``` + +### Protected Endpoints (Authentication Required) + +All protected endpoints require a valid JWT token in the Authorization header: +``` +Authorization: Bearer +``` + +#### GET /api/v1/auth/me +Get information about the currently authenticated user. + +**Headers:** +``` +Authorization: Bearer +``` + +**Response:** +```json +{ + "id": "1", + "username": "admin", + "email": "admin@example.com", + "role": "admin" +} +``` + +## Demo Users + +The system comes with pre-configured demo users: + +| Username | Password | Role | +|----------|----------|-------| +| admin | password | admin | +| user | password | user | + +## Using the JWT Token + +Once you receive a JWT token from the login endpoint, include it in the Authorization header for all protected endpoints: + +```bash +curl -X GET http://localhost:8080/api/v1/products \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +## Token Expiration + +JWT tokens expire after 1 hour. When a token expires, you'll need to login again to get a new token. + +## Environment Variables + +To configure JWT authentication, you can set these environment variables: + +```bash +# JWT Secret (change this in production) +JWT_SECRET=your-secret-key-change-this-in-production +``` + +## Testing the Authentication + +### 1. Login with demo user +```bash +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"password"}' +``` + +### 2. Use the token to access protected endpoints +```bash +# Get user info +curl -X GET http://localhost:8080/api/v1/auth/me \ + -H "Authorization: Bearer " + +# Get products +curl -X GET http://localhost:8080/api/v1/products \ + -H "Authorization: Bearer " +``` + +## Error Handling + +The API returns appropriate HTTP status codes: +- `200 OK` - Successful request +- `201 Created` - Resource created successfully +- `400 Bad Request` - Invalid request data +- `401 Unauthorized` - Invalid or missing token +- `500 Internal Server Error` - Server error diff --git a/cmd/api/docs/docs.go b/cmd/api/docs/docs.go index 8601e0f2..f628fda6 100644 --- a/cmd/api/docs/docs.go +++ b/cmd/api/docs/docs.go @@ -48,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", @@ -109,6 +299,290 @@ const docTemplate = `{ } } }, + "/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", @@ -215,6 +689,129 @@ const docTemplate = `{ "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" + } + } } } }` diff --git a/cmd/api/docs/swagger.json b/cmd/api/docs/swagger.json index 517be521..de164848 100644 --- a/cmd/api/docs/swagger.json +++ b/cmd/api/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", @@ -106,6 +296,290 @@ } } }, + "/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", @@ -212,6 +686,129 @@ "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 index 69543b55..b190e41b 100644 --- a/cmd/api/docs/swagger.yaml +++ b/cmd/api/docs/swagger.yaml @@ -49,6 +49,86 @@ definitions: 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: @@ -78,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: @@ -118,6 +321,194 @@ paths: 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: diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 00000000..5b664b14 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,498 @@ +// 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}}", + "/": { + "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/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" + } + } + } + } + }, + "/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.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" + } + } + } + } +}` + +// 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/docs/swagger.json b/docs/swagger.json new file mode 100644 index 00000000..01963090 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,480 @@ +{ + "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/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" + } + } + } + } + }, + "/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.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" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 00000000..e478bcaa --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,316 @@ +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.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 +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/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 + /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/internal/handlers/auth.go b/internal/handlers/auth.go new file mode 100644 index 00000000..7c5be4b7 --- /dev/null +++ b/internal/handlers/auth.go @@ -0,0 +1,132 @@ +package handlers + +import ( + "api-service/internal/models" + "api-service/internal/services" + "net/http" + + "github.com/gin-gonic/gin" +) + +// AuthHandler handles authentication endpoints +type AuthHandler struct { + authService *services.AuthService +} + +// NewAuthHandler creates a new authentication handler +func NewAuthHandler(authService *services.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +// Login godoc +// @Summary Login user and get JWT token +// @Description Authenticate user with username and password to receive JWT token +// @Tags Authentication +// @Accept json +// @Produce json +// @Param login body models.LoginRequest true "Login credentials" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/auth/login [post] +func (h *AuthHandler) Login(c *gin.Context) { + var loginReq models.LoginRequest + + // Bind JSON request + if err := c.ShouldBindJSON(&loginReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Authenticate user + tokenResponse, err := h.authService.Login(loginReq.Username, loginReq.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tokenResponse) +} + +// RefreshToken godoc +// @Summary Refresh JWT token +// @Description Refresh the JWT token using a valid refresh token +// @Tags Authentication +// @Accept json +// @Produce json +// @Param refresh body map[string]string true "Refresh token" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/auth/refresh [post] +func (h *AuthHandler) RefreshToken(c *gin.Context) { + // For now, this is a placeholder for refresh token functionality + // In a real implementation, you would handle refresh tokens here + c.JSON(http.StatusNotImplemented, gin.H{"error": "refresh token not implemented"}) +} + +// Register godoc +// @Summary Register new user +// @Description Register a new user account +// @Tags Authentication +// @Accept json +// @Produce json +// @Param register body map[string]string true "Registration data" +// @Success 201 {object} map[string]string +// @Failure 400 {object} map[string]string "Bad request" +// @Router /api/v1/auth/register [post] +func (h *AuthHandler) Register(c *gin.Context) { + var registerReq struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Role string `json:"role" binding:"required"` + } + + if err := c.ShouldBindJSON(®isterReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + err := h.authService.RegisterUser( + registerReq.Username, + registerReq.Email, + registerReq.Password, + registerReq.Role, + ) + + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "user registered successfully"}) +} + +// Me godoc +// @Summary Get current user info +// @Description Get information about the currently authenticated user +// @Tags Authentication +// @Produce json +// @Security Bearer +// @Success 200 {object} models.User +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/auth/me [get] +func (h *AuthHandler) Me(c *gin.Context) { + // Get user info from context (set by middleware) + userID, exists := c.Get("user_id") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) + return + } + + // In a real implementation, you would fetch user details from database + c.JSON(http.StatusOK, gin.H{ + "id": userID, + "username": c.GetString("username"), + "email": c.GetString("email"), + "role": c.GetString("role"), + }) +} diff --git a/internal/handlers/product.go b/internal/handlers/product.go new file mode 100644 index 00000000..ad5d503c --- /dev/null +++ b/internal/handlers/product.go @@ -0,0 +1,124 @@ +package handlers + +import ( + "api-service/internal/models" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ProductHandler handles product services +type ProductHandler struct{} + +// NewProductHandler creates a new ProductHandler +func NewProductHandler() *ProductHandler { + return &ProductHandler{} +} + +// GetProduct godoc +// @Summary Get product +// @Description Returns a list of products +// @Tags product +// @Accept json +// @Produce json +// @Success 200 {object} models.ProductGetResponse "Product GET response" +// @Router /api/v1/products [get] +func (h *ProductHandler) GetProduct(c *gin.Context) { + response := models.ProductGetResponse{ + Message: "List of products", + Data: []string{"Product 1", "Product 2"}, + } + c.JSON(http.StatusOK, response) +} + +// GetProductByID godoc +// @Summary Get product by ID +// @Description Returns a single product by ID +// @Tags product +// @Accept json +// @Produce json +// @Param id path string true "Product ID" +// @Success 200 {object} models.ProductGetByIDResponse "Product GET by ID response" +// @Failure 404 {object} models.ErrorResponse "Product not found" +// @Router /api/v1/products/{id} [get] +func (h *ProductHandler) GetProductByID(c *gin.Context) { + id := c.Param("id") + response := models.ProductGetByIDResponse{ + ID: id, + Message: "Product details", + } + c.JSON(http.StatusOK, response) +} + +// CreateProduct godoc +// @Summary Create product +// @Description Creates a new product +// @Tags product +// @Accept json +// @Produce json +// @Param request body models.ProductCreateRequest true "Product creation request" +// @Success 201 {object} models.ProductCreateResponse "Product created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Router /api/v1/products [post] +func (h *ProductHandler) CreateProduct(c *gin.Context) { + var req models.ProductCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response := models.ProductCreateResponse{ + ID: uuid.NewString(), + Message: "Product created successfully", + Data: req, + } + c.JSON(http.StatusCreated, response) +} + +// UpdateProduct godoc +// @Summary Update product +// @Description Updates an existing product +// @Tags product +// @Accept json +// @Produce json +// @Param id path string true "Product ID" +// @Param request body models.ProductUpdateRequest true "Product update request" +// @Success 200 {object} models.ProductUpdateResponse "Product updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 404 {object} models.ErrorResponse "Product not found" +// @Router /api/v1/products/{id} [put] +func (h *ProductHandler) UpdateProduct(c *gin.Context) { + id := c.Param("id") + var req models.ProductUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response := models.ProductUpdateResponse{ + ID: id, + Message: "Product updated successfully", + Data: req, + } + c.JSON(http.StatusOK, response) +} + +// DeleteProduct godoc +// @Summary Delete product +// @Description Deletes a product by ID +// @Tags product +// @Accept json +// @Produce json +// @Param id path string true "Product ID" +// @Success 200 {object} models.ProductDeleteResponse "Product deleted successfully" +// @Failure 404 {object} models.ErrorResponse "Product not found" +// @Router /api/v1/products/{id} [delete] +func (h *ProductHandler) DeleteProduct(c *gin.Context) { + id := c.Param("id") + response := models.ProductDeleteResponse{ + ID: id, + Message: "Product deleted successfully", + } + c.JSON(http.StatusOK, response) +} diff --git a/internal/handlers/token_handler.go b/internal/handlers/token_handler.go new file mode 100644 index 00000000..5cce937f --- /dev/null +++ b/internal/handlers/token_handler.go @@ -0,0 +1,95 @@ +package handlers + +import ( + "api-service/internal/models" + "api-service/internal/services" + "net/http" + + "github.com/gin-gonic/gin" +) + +// TokenHandler handles token generation endpoints +type TokenHandler struct { + authService *services.AuthService +} + +// NewTokenHandler creates a new token handler +func NewTokenHandler(authService *services.AuthService) *TokenHandler { + return &TokenHandler{ + authService: authService, + } +} + +// GenerateToken godoc +// @Summary Generate JWT token +// @Description Generate a JWT token for a user +// @Tags Token +// @Accept json +// @Produce json +// @Param token body models.LoginRequest true "User credentials" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 401 {object} map[string]string "Unauthorized" +// @Router /api/v1/token/generate [post] +func (h *TokenHandler) GenerateToken(c *gin.Context) { + var loginReq models.LoginRequest + + // Bind JSON request + if err := c.ShouldBindJSON(&loginReq); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Generate token + tokenResponse, err := h.authService.Login(loginReq.Username, loginReq.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, tokenResponse) +} + +// GenerateTokenDirect godoc +// @Summary Generate token directly +// @Description Generate a JWT token directly without password verification (for testing) +// @Tags Token +// @Accept json +// @Produce json +// @Param user body map[string]string true "User info" +// @Success 200 {object} models.TokenResponse +// @Failure 400 {object} map[string]string "Bad request" +// @Router /api/v1/token/generate-direct [post] +func (h *TokenHandler) GenerateTokenDirect(c *gin.Context) { + var req struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required"` + Role string `json:"role" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Create a temporary user for token generation + user := &models.User{ + ID: "temp-" + req.Username, + Username: req.Username, + Email: req.Email, + Role: req.Role, + } + + // Generate token directly + token, err := h.authService.GenerateTokenForUser(user) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, models.TokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresIn: 3600, + }) +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 6bde2557..a88bc46a 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -197,30 +197,30 @@ func AuthMiddleware() gin.HandlerFunc { // "github.com/gin-gonic/gin" // ) -// // AuthMiddleware validates Bearer token in Authorization header -// func AuthMiddleware() gin.HandlerFunc { -// return func(c *gin.Context) { -// authHeader := c.GetHeader("Authorization") -// if authHeader == "" { -// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"}) -// return -// } +// AuthMiddleware validates Bearer token in Authorization header +func AuthJWTMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"}) + return + } -// parts := strings.SplitN(authHeader, " ", 2) -// if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { -// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) -// return -// } + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) + return + } -// token := parts[1] -// // For now, use a static token for validation. Replace with your logic. -// const validToken = "your-static-token" + token := parts[1] + // For now, use a static token for validation. Replace with your logic. + const validToken = "your-static-token" -// if token != validToken { -// c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) -// return -// } + if token != validToken { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + return + } -// c.Next() -// } -// } + c.Next() + } +} diff --git a/internal/middleware/jwt_middleware.go b/internal/middleware/jwt_middleware.go new file mode 100644 index 00000000..e3dee8a3 --- /dev/null +++ b/internal/middleware/jwt_middleware.go @@ -0,0 +1,77 @@ +package middleware + +import ( + "api-service/internal/services" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// JWTAuthMiddleware validates JWT tokens generated by our auth service +func JWTAuthMiddleware(authService *services.AuthService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"}) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) + return + } + + tokenString := parts[1] + + // Validate token + claims, err := authService.ValidateToken(tokenString) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + // Set user info in context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("email", claims.Email) + c.Set("role", claims.Role) + + c.Next() + } +} + +// OptionalAuthMiddleware allows both authenticated and unauthenticated requests +func OptionalAuthMiddleware(authService *services.AuthService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + // No token provided, but continue + c.Next() + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + c.Next() + return + } + + tokenString := parts[1] + claims, err := authService.ValidateToken(tokenString) + if err != nil { + // Invalid token, but continue (don't abort) + c.Next() + return + } + + // Set user info in context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("email", claims.Email) + c.Set("role", claims.Role) + + c.Next() + } +} diff --git a/internal/models/auth.go b/internal/models/auth.go new file mode 100644 index 00000000..872b45ab --- /dev/null +++ b/internal/models/auth.go @@ -0,0 +1,31 @@ +package models + +// LoginRequest represents the login request payload +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// TokenResponse represents the token response +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` +} + +// JWTClaims represents the JWT claims +type JWTClaims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Email string `json:"email"` + Role string `json:"role"` +} + +// User represents a user for authentication +type User struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"-"` + Role string `json:"role"` +} diff --git a/internal/models/product.go b/internal/models/product.go new file mode 100644 index 00000000..97210844 --- /dev/null +++ b/internal/models/product.go @@ -0,0 +1,50 @@ +package models + +// ProductGetResponse represents the response for GET products +type ProductGetResponse struct { + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// ProductGetByIDResponse represents the response for GET product by ID +type ProductGetByIDResponse struct { + ID string `json:"id"` + Message string `json:"message"` +} + +// ProductCreateRequest represents the request for creating product +type ProductCreateRequest struct { + Name string `json:"name" binding:"required"` + // Add more fields as needed +} + +// ProductCreateResponse represents the response for creating product +type ProductCreateResponse struct { + ID string `json:"id"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// ProductUpdateRequest represents the request for updating product +type ProductUpdateRequest struct { + Name string `json:"name" binding:"required"` + // Add more fields as needed +} + +// ProductUpdateResponse represents the response for updating product +type ProductUpdateResponse struct { + ID string `json:"id"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +// ProductDeleteResponse represents the response for deleting product +type ProductDeleteResponse struct { + ID string `json:"id"` + Message string `json:"message"` +} + +// ErrorResponse represents an error response +// type ErrorResponse struct { +// Error string `json:"error"` +// } diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 05102cd0..9c835f6a 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -3,8 +3,10 @@ package v1 import ( "net/http" + "api-service/internal/config" "api-service/internal/handlers" "api-service/internal/middleware" + "api-service/internal/services" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" @@ -12,37 +14,64 @@ import ( ) // RegisterRoutes registers all API routes for version 1 -func RegisterRoutes() *gin.Engine { +func RegisterRoutes(cfg *config.Config) *gin.Engine { router := gin.New() // Add middleware router.Use(middleware.CORSConfig()) router.Use(middleware.ErrorHandler()) - // router.Use(middleware.AuthMiddleware()) // Added auth middleware here router.Use(gin.Logger()) router.Use(gin.Recovery()) + // Initialize services + authService := services.NewAuthService(cfg) + // Swagger UI route router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // API v1 group v1 := router.Group("/api/v1") { - router.Use(middleware.AuthMiddleware()) // Added auth middleware here + // Public routes (no authentication required) // Health endpoints healthHandler := handlers.NewHealthHandler() v1.GET("/health", healthHandler.GetHealth) v1.GET("/", healthHandler.HelloWorld) - // Example endpoints - exampleHandler := handlers.NewExampleHandler() - v1.GET("/example", exampleHandler.GetExample) - v1.POST("/example", exampleHandler.PostExample) + // Authentication routes + authHandler := handlers.NewAuthHandler(authService) + tokenHandler := handlers.NewTokenHandler(authService) - // WebSocket endpoint - v1.GET("/websocket", WebSocketHandler) + v1.POST("/auth/login", authHandler.Login) + v1.POST("/auth/register", authHandler.Register) + v1.GET("/auth/me", middleware.JWTAuthMiddleware(authService), authHandler.Me) + v1.POST("/auth/refresh", authHandler.RefreshToken) - v1.GET("/webservice", WebServiceHandler) + // Token generation routes + v1.POST("/token/generate", tokenHandler.GenerateToken) + v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect) + + // Protected routes (require authentication) + protected := v1.Group("/") + protected.Use(middleware.JWTAuthMiddleware(authService)) + { + // Product endpoints + productHandler := handlers.NewProductHandler() + protected.GET("/products", productHandler.GetProduct) + protected.GET("/products/:id", productHandler.GetProductByID) + protected.POST("/products", productHandler.CreateProduct) + protected.PUT("/products/:id", productHandler.UpdateProduct) + protected.DELETE("/products/:id", productHandler.DeleteProduct) + + // Example endpoints + exampleHandler := handlers.NewExampleHandler() + protected.GET("/example", exampleHandler.GetExample) + protected.POST("/example", exampleHandler.PostExample) + + // WebSocket endpoint + protected.GET("/websocket", WebSocketHandler) + protected.GET("/webservice", WebServiceHandler) + } } return router diff --git a/internal/server/routes.go b/internal/server/routes.go deleted file mode 100644 index 14b98726..00000000 --- a/internal/server/routes.go +++ /dev/null @@ -1,71 +0,0 @@ -package server - -import ( - "net/http" - - "fmt" - "log" - "time" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" - - "github.com/coder/websocket" -) - -func (s *Server) RegisterRoutes() http.Handler { - r := gin.Default() - - r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"http://localhost:5173"}, // Add your frontend URL - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, - AllowHeaders: []string{"Accept", "Authorization", "Content-Type"}, - AllowCredentials: true, // Enable cookies/auth - })) - - r.GET("/", s.HelloWorldHandler) - - r.GET("/health", s.healthHandler) - - r.GET("/websocket", s.websocketHandler) - - return r -} - -func (s *Server) HelloWorldHandler(c *gin.Context) { - resp := make(map[string]string) - resp["message"] = "Hello World" - - c.JSON(http.StatusOK, resp) -} - -func (s *Server) healthHandler(c *gin.Context) { - c.JSON(http.StatusOK, s.db.Health()) -} - -func (s *Server) websocketHandler(c *gin.Context) { - w := c.Writer - r := c.Request - socket, err := websocket.Accept(w, r, nil) - - if err != nil { - log.Printf("could not open websocket: %v", err) - _, _ = w.Write([]byte("could not open websocket")) - w.WriteHeader(http.StatusInternalServerError) - return - } - - defer socket.Close(websocket.StatusGoingAway, "server closing websocket") - - ctx := r.Context() - socketCtx := socket.CloseRead(ctx) - - for { - payload := fmt.Sprintf("server timestamp: %d", time.Now().UnixNano()) - err := socket.Write(socketCtx, websocket.MessageText, []byte(payload)) - if err != nil { - break - } - time.Sleep(time.Second * 2) - } -} diff --git a/internal/server/routes_test.go b/internal/server/routes_test.go deleted file mode 100644 index db2233ea..00000000 --- a/internal/server/routes_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "net/http" - "net/http/httptest" - "testing" -) - -func TestHelloWorldHandler(t *testing.T) { - s := &Server{} - r := gin.New() - r.GET("/", s.HelloWorldHandler) - // Create a test HTTP request - req, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } - // Create a ResponseRecorder to record the response - rr := httptest.NewRecorder() - // Serve the HTTP request - r.ServeHTTP(rr, req) - // Check the status code - if status := rr.Code; status != http.StatusOK { - t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - // Check the response body - expected := "{\"message\":\"Hello World\"}" - if rr.Body.String() != expected { - t.Errorf("Handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } -} diff --git a/internal/server/server.go b/internal/server/server.go index 5c5230fc..4a0b8252 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -37,7 +37,7 @@ func NewServer() *http.Server { // Declare Server config server := &http.Server{ Addr: fmt.Sprintf(":%d", NewServer.port), - Handler: v1.RegisterRoutes(), + Handler: v1.RegisterRoutes(cfg), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go new file mode 100644 index 00000000..f46f24d1 --- /dev/null +++ b/internal/services/auth_service.go @@ -0,0 +1,169 @@ +package services + +import ( + "api-service/internal/config" + "api-service/internal/models" + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +// AuthService handles authentication logic +type AuthService struct { + config *config.Config + users map[string]*models.User // In-memory user store for demo +} + +// NewAuthService creates a new authentication service +func NewAuthService(cfg *config.Config) *AuthService { + // Initialize with demo users + users := make(map[string]*models.User) + + // Add demo users + users["admin"] = &models.User{ + ID: "1", + Username: "admin", + Email: "admin@example.com", + Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // password + Role: "admin", + } + + users["user"] = &models.User{ + ID: "2", + Username: "user", + Email: "user@example.com", + Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // password + Role: "user", + } + + return &AuthService{ + config: cfg, + users: users, + } +} + +// Login authenticates user and generates JWT token +func (s *AuthService) Login(username, password string) (*models.TokenResponse, error) { + user, exists := s.users[username] + if !exists { + return nil, errors.New("invalid credentials") + } + + // Verify password + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + if err != nil { + return nil, errors.New("invalid credentials") + } + + // Generate JWT token + token, err := s.generateToken(user) + if err != nil { + return nil, err + } + + return &models.TokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresIn: 3600, // 1 hour + }, nil +} + +// generateToken creates a new JWT token for the user +func (s *AuthService) generateToken(user *models.User) (string, error) { + // Create claims + claims := jwt.MapClaims{ + "user_id": user.ID, + "username": user.Username, + "email": user.Email, + "role": user.Role, + "exp": time.Now().Add(time.Hour * 1).Unix(), + "iat": time.Now().Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign token with secret key + secretKey := []byte(s.getJWTSecret()) + return token.SignedString(secretKey) +} + +// GenerateTokenForUser generates a JWT token for a specific user +func (s *AuthService) GenerateTokenForUser(user *models.User) (string, error) { + // Create claims + claims := jwt.MapClaims{ + "user_id": user.ID, + "username": user.Username, + "email": user.Email, + "role": user.Role, + "exp": time.Now().Add(time.Hour * 1).Unix(), + "iat": time.Now().Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign token with secret key + secretKey := []byte(s.getJWTSecret()) + return token.SignedString(secretKey) +} + +// ValidateToken validates the JWT token +func (s *AuthService) ValidateToken(tokenString string) (*models.JWTClaims, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return []byte(s.getJWTSecret()), nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, errors.New("invalid claims") + } + + return &models.JWTClaims{ + UserID: claims["user_id"].(string), + Username: claims["username"].(string), + Email: claims["email"].(string), + Role: claims["role"].(string), + }, nil +} + +// getJWTSecret returns the JWT secret key +func (s *AuthService) getJWTSecret() string { + // In production, this should come from environment variables + return "your-secret-key-change-this-in-production" +} + +// RegisterUser registers a new user (for demo purposes) +func (s *AuthService) RegisterUser(username, email, password, role string) error { + if _, exists := s.users[username]; exists { + return errors.New("username already exists") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + + s.users[username] = &models.User{ + ID: string(rune(len(s.users) + 1)), + Username: username, + Email: email, + Password: string(hashedPassword), + Role: role, + } + + return nil +} diff --git a/tools/HANDLER.md b/tools/HANDLER.md new file mode 100644 index 00000000..d3fa68fc --- /dev/null +++ b/tools/HANDLER.md @@ -0,0 +1,111 @@ +# Handler Generator CLI Tool + +CLI tool untuk generate handler baru secara otomatis dengan swagger documentation. + +## Cara Penggunaan + +### Windows +```bash +# Buka terminal di folder tools +generate.bat [methods] + +# Contoh: +generate.bat user get post +generate.bat product get post put delete +``` + +### Linux/Mac +```bash +# Buka terminal di folder tools +./generate.sh [methods] + +# Contoh: +./generate.sh user get post +./generate.sh 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 user get post put delete +``` + +## Method yang Tersedia +- `get` - GET endpoint untuk list data +- `post` - POST endpoint untuk create data +- `put` - PUT endpoint untuk update data +- `delete` - DELETE endpoint untuk delete data + +## File yang Dibuat Otomatis + +1. **Handler**: `internal/handlers/.go` +2. **Models**: `internal/models/.go` +3. **Routes**: Update otomatis di `internal/routes/v1/routes.go` + +## Contoh Penggunaan + +### 1. Generate Handler dengan GET dan POST +```bash +./generate.sh user get post +``` + +### 2. Generate Handler dengan semua method +```bash +./generate.sh product get post put delete +``` + +### 3. Generate Handler dengan custom method +```bash +./generate.sh order get post delete +``` + +## Langkah Setelah Generate + +1. Jalankan swagger generator: +```bash +swag init -g cmd/api/main.go --output cmd/api/docs +``` + +2. Jalankan aplikasi: +```bash +go run cmd/api/main.go +``` + +3. Akses swagger UI: +``` +http://localhost:8080/swagger/index.html +``` + +## Struktur File yang Dibuat + +### Handler File (`internal/handlers/.go`) +- Struct handler +- Constructor function +- Endpoint methods dengan swagger documentation +- Error handling + +### Model File (`internal/models/.go`) +- Request models +- Response models +- Error response models + +### Routes Update +- Otomatis menambahkan routes ke `/api/v1/` +- Support parameter ID untuk endpoint spesifik + +## Contoh Output + +Untuk command: `./generate.sh user get post` + +### Handler yang dibuat: +- `GET /api/v1/users` - List users +- `GET /api/v1/users/:id` - Get user by ID +- `POST /api/v1/users` - Create new user + +### Swagger Documentation +Semua endpoint otomatis memiliki swagger documentation yang bisa diakses di: +``` +http://localhost:8080/swagger/index.html diff --git a/tools/generate-handler.go b/tools/generate-handler.go new file mode 100644 index 00000000..d860fb29 --- /dev/null +++ b/tools/generate-handler.go @@ -0,0 +1,364 @@ +package main + +import ( + "fmt" + "os" + "strings" + "text/template" + "time" +) + +// HandlerData contains template data for handler generation +type HandlerData struct { + Name string + NameLower string + NamePlural string + ModuleName string + HasGet bool + HasPost bool + HasPut bool + HasDelete bool + HasParam bool + HasRequest bool + HasResponse bool + Timestamp string +} + +func main() { + if len(os.Args) < 2 { + 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)") + os.Exit(1) + } + + handlerName := strings.Title(os.Args[1]) + methods := []string{"get", "post"} + if len(os.Args) > 2 { + methods = os.Args[2:] + } + + // Convert to lowercase for file names + handlerLower := strings.ToLower(handlerName) + handlerPlural := handlerLower + "s" + + data := HandlerData{ + Name: handlerName, + NameLower: handlerLower, + NamePlural: handlerPlural, + ModuleName: "api-service", + Timestamp: time.Now().Format("2006-01-02 15:04:05"), + } + + // Check which methods are requested + for _, method := range methods { + switch strings.ToLower(method) { + case "get": + data.HasGet = true + case "post": + data.HasPost = true + case "put": + data.HasPut = true + case "delete": + data.HasDelete = true + } + } + + // Check if we need request/response models + data.HasRequest = data.HasPost || data.HasPut + data.HasResponse = true + data.HasParam = data.HasGet || data.HasPut || data.HasDelete + + fmt.Printf("Generating handler: %s with methods: %v\n", handlerName, methods) + + // Create directories if they don't exist + os.MkdirAll("internal/handlers", 0755) + os.MkdirAll("internal/models", 0755) + + // Generate files + generateHandlerFile(data) + generateModelFile(data) + updateRoutesFile(data) + + fmt.Printf("Successfully generated handler: %s\n", handlerName) + fmt.Println("Don't forget to run: swag init -g cmd/api/main.go") +} + +func generateHandlerFile(data HandlerData) { + handlerTemplate := `package handlers + +import ( + "net/http" + "strings" + "{{.ModuleName}}/internal/models" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// {{.Name}}Handler handles {{.NameLower}} services +type {{.Name}}Handler 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}} +// @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"}, + } + 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}} +// @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) { + id := c.Param("id") + response := models.{{.Name}}GetByIDResponse{ + ID: id, + Message: "{{.Name}} details", + } + c.JSON(http.StatusOK, response) +} +{{end}} +{{if .HasPost}} +// Create{{.Name}} godoc +// @Summary Create {{.NameLower}} +// @Description Creates a new {{.NameLower}} +// @Tags {{.NameLower}} +// @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 + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response := models.{{.Name}}CreateResponse{ + ID: uuid.NewString(), + Message: "{{.Name}} 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}} +// @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) { + id := c.Param("id") + var req models.{{.Name}}UpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response := models.{{.Name}}UpdateResponse{ + ID: id, + Message: "{{.Name}} 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}} +// @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) { + id := c.Param("id") + response := models.{{.Name}}DeleteResponse{ + ID: id, + Message: "{{.Name}} deleted successfully", + } + c.JSON(http.StatusOK, response) +} +{{end}} +` + + writeFile("internal/handlers/"+data.NameLower+".go", handlerTemplate, data) +} + +func generateModelFile(data HandlerData) { + modelTemplate := `package models + +{{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}} +} +{{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}} +} +{{end}} +{{if .HasPost}} +// {{.Name}}CreateRequest represents the request for creating {{.NameLower}} +type {{.Name}}CreateRequest struct { + Name string {{.Backtick}}json:"name" binding:"required"{{.Backtick}} + // 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}} +} +{{end}} +{{if .HasPut}} +// {{.Name}}UpdateRequest represents the request for updating {{.NameLower}} +type {{.Name}}UpdateRequest struct { + Name string {{.Backtick}}json:"name" binding:"required"{{.Backtick}} + // 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}} +} +{{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}} + +// ErrorResponse represents an error response +type ErrorResponse struct { + Error string {{.Backtick}}json:"error"{{.Backtick}} +} +` + + // Replace backtick with actual backtick + modelTemplate = strings.ReplaceAll(modelTemplate, "{{.Backtick}}", "`") + writeFile("internal/models/"+data.NameLower+".go", modelTemplate, data) +} + +func updateRoutesFile(data HandlerData) { + routesFile := "internal/routes/v1/routes.go" + + // Read existing routes file + content, err := os.ReadFile(routesFile) + if err != nil { + fmt.Printf("Error reading routes file: %v\n", err) + return + } + + // Convert to string + routesContent := string(content) + + // Find the place to insert new routes + insertMarker := "\t\t// Example endpoints" + + // Generate new routes + newRoutes := fmt.Sprintf("\t\t// %s endpoints\n", 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) + } + + if data.HasPut { + newRoutes += fmt.Sprintf("\t\tv1.PUT(\"/%s/:id\", %sHandler.Update%s)\n", data.NamePlural, data.NameLower, data.Name) + } + + if data.HasDelete { + newRoutes += fmt.Sprintf("\t\tv1.DELETE(\"/%s/:id\", %sHandler.Delete%s)\n", data.NamePlural, data.NameLower, data.Name) + } + + newRoutes += "\n" + + // Insert new routes after the marker + newContent := strings.Replace(routesContent, insertMarker, insertMarker+"\n"+newRoutes, 1) + + // Write back to file + err = os.WriteFile(routesFile, []byte(newContent), 0644) + if err != nil { + fmt.Printf("Error writing routes file: %v\n", err) + return + } +} + +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) + 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) +} diff --git a/tools/generate.bat b/tools/generate.bat new file mode 100644 index 00000000..eb7d0928 --- /dev/null +++ b/tools/generate.bat @@ -0,0 +1,28 @@ +@echo off +REM Handler Generator Script for Windows +REM Usage: generate.bat [methods] + +if "%~1"=="" ( + echo Usage: generate.bat ^ [methods] + echo Example: generate.bat user get post put delete + echo Methods: get, post, put, delete (optional, default: get post) + pause + exit /b 1 +) + +set HANDLER_NAME=%~1 +shift + +set METHODS=%* +if "%METHODS%"=="" set METHODS=get post + +echo Generating handler: %HANDLER_NAME% with methods: %METHODS% +echo. + +cd /d "%~dp0.." +go run tools/generate-handler.go %HANDLER_NAME% %METHODS% + +echo. +echo Handler generated successfully! +echo Don't forget to run: swag init -g cmd/api/main.go +pause diff --git a/tools/generate.sh b/tools/generate.sh new file mode 100644 index 00000000..c0abfd13 --- /dev/null +++ b/tools/generate.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Handler Generator Script for Unix/Linux/Mac +# Usage: ./generate.sh [methods] + +set -e + +if [ $# -lt 1 ]; then + echo "Usage: $0 [methods]" + echo "Example: $0 user get post put delete" + echo "Methods: get, post, put, delete (optional, default: get post)" + exit 1 +fi + +HANDLER_NAME=$1 +shift + +METHODS=$@ +if [ -z "$METHODS" ]; then + METHODS="get post" +fi + +echo "Generating handler: $HANDLER_NAME with methods: $METHODS" +echo + +# Change to project root directory +cd "$(dirname "$0")/.." + +# Run the generator +go run tools/generate-handler.go "$HANDLER_NAME" $METHODS + +echo +echo "Handler generated successfully!" +echo "Don't forget to run: swag init -g cmd/api/main.go"