Update template go

This commit is contained in:
2025-08-14 09:08:34 +07:00
parent 8c6fb0ce48
commit a64cbf4438
23 changed files with 4297 additions and 137 deletions

141
AUTHENTICATION.md Normal file
View File

@@ -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 <your-jwt-token>
```
#### GET /api/v1/auth/me
Get information about the currently authenticated user.
**Headers:**
```
Authorization: Bearer <your-jwt-token>
```
**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 <your-token>"
# Get products
curl -X GET http://localhost:8080/api/v1/products \
-H "Authorization: Bearer <your-token>"
```
## 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

View File

@@ -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"
}
}
}
}
}`

View File

@@ -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"
}
}
}
}
}

View File

@@ -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:

498
docs/docs.go Normal file
View File

@@ -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)
}

480
docs/swagger.json Normal file
View File

@@ -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"
}
}
}
}
}

316
docs/swagger.yaml Normal file
View File

@@ -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"

132
internal/handlers/auth.go Normal file
View File

@@ -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(&registerReq); 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"),
})
}

View File

@@ -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)
}

View File

@@ -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,
})
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

31
internal/models/auth.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
// }

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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
}

111
tools/HANDLER.md Normal file
View File

@@ -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 <nama-handler> [methods]
# Contoh:
generate.bat user get post
generate.bat product get post put delete
```
### Linux/Mac
```bash
# Buka terminal di folder tools
./generate.sh <nama-handler> [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/<nama>.go`
2. **Models**: `internal/models/<nama>.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/<nama>.go`)
- Struct handler
- Constructor function
- Endpoint methods dengan swagger documentation
- Error handling
### Model File (`internal/models/<nama>.go`)
- Request models
- Response models
- Error response models
### Routes Update
- Otomatis menambahkan routes ke `/api/v1/<nama-plural>`
- 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

364
tools/generate-handler.go Normal file
View File

@@ -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 <handler-name> [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)
}

28
tools/generate.bat Normal file
View File

@@ -0,0 +1,28 @@
@echo off
REM Handler Generator Script for Windows
REM Usage: generate.bat <handler-name> [methods]
if "%~1"=="" (
echo Usage: generate.bat ^<handler-name^> [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

33
tools/generate.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Handler Generator Script for Unix/Linux/Mac
# Usage: ./generate.sh <handler-name> [methods]
set -e
if [ $# -lt 1 ]; then
echo "Usage: $0 <handler-name> [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"