From 93ecaf71dc264753f2c0a764986b65cc964a2d1a Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Fri, 15 Nov 2024 23:45:54 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20feat:=20add=20swagger=20document?= =?UTF-8?q?ation=20and=20improve=20project=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ feat: add API documentation with swagger and OpenAPI specs ♻️ refactor: reorganize server initialization and cleanup process 🔧 fix: update build paths in air.toml and Dockerfile The changes include: 1. Adding swagger documentation for API endpoints 2. Restructuring server initialization with proper cleanup 3. Moving main.go to root directory 4. Updating build configuration files 5. Adding API documentation endpoints and handlers --- .air.toml | 2 +- .gitignore | 1 - Dockerfile | 2 +- docs/docs.go | 105 ++++++++++++++++++++++++ docs/swagger.json | 81 ++++++++++++++++++ docs/swagger.yaml | 53 ++++++++++++ go.mod | 17 ++++ go.sum | 45 ++++++++++ internal/middleware/auth.go | 93 --------------------- internal/server/handlers/apikey.go | 11 ++- internal/server/middleware/ratelimit.go | 25 +++++- internal/server/server.go | 24 +----- internal/server/services/apikey.go | 43 ++++++---- cmd/server/main.go => main.go | 7 ++ 14 files changed, 373 insertions(+), 136 deletions(-) create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml delete mode 100644 internal/middleware/auth.go rename cmd/server/main.go => main.go (88%) diff --git a/.air.toml b/.air.toml index 1077937..66df3b8 100644 --- a/.air.toml +++ b/.air.toml @@ -5,7 +5,7 @@ tmp_dir = "tmp" [build] args_bin = [] bin = "./tmp/main" - cmd = "go build -o ./tmp/main ./cmd/server" + cmd = "go build -o ./tmp/main ." delay = 1000 exclude_dir = ["assets", "tmp", "vendor", "testdata"] exclude_file = [] diff --git a/.gitignore b/.gitignore index d8e7284..a9ec462 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -/docs/ /lib/ /bin/ /uploads/ diff --git a/Dockerfile b/Dockerfile index cafb14d..1998421 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY . . RUN mkdir -p /build/src/app -RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o app/0x45 ./cmd/server/main.go +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o app/0x45 . FROM scratch diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..06e63a2 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,105 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "license": { + "name": "MIT", + "url": "https://github.com/watzon/0x45/blob/main/LICENSE" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/keys/request": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API Key" + ], + "summary": "Request a new API key", + "operationId": "HandleRequestAPIKey", + "parameters": [ + { + "description": "Request a new API key", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/services.APIKeyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/services.APIKeyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/services.APIKeyResponse" + } + } + } + } + } + }, + "definitions": { + "services.APIKeyRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "services.APIKeyResponse": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:3000", + BasePath: "/", + Schemes: []string{}, + Title: "0x45 API", + Description: "API for 0x45", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..c6e4ab2 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,81 @@ +{ + "swagger": "2.0", + "info": { + "description": "API for 0x45", + "title": "0x45 API", + "contact": {}, + "license": { + "name": "MIT", + "url": "https://github.com/watzon/0x45/blob/main/LICENSE" + }, + "version": "1.0" + }, + "host": "localhost:3000", + "basePath": "/", + "paths": { + "/api/keys/request": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API Key" + ], + "summary": "Request a new API key", + "operationId": "HandleRequestAPIKey", + "parameters": [ + { + "description": "Request a new API key", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/services.APIKeyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/services.APIKeyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/services.APIKeyResponse" + } + } + } + } + } + }, + "definitions": { + "services.APIKeyRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "services.APIKeyResponse": { + "type": "object", + "properties": { + "key": { + "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 0000000..1adf461 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,53 @@ +basePath: / +definitions: + services.APIKeyRequest: + properties: + email: + type: string + name: + type: string + type: object + services.APIKeyResponse: + properties: + key: + type: string + message: + type: string + type: object +host: localhost:3000 +info: + contact: {} + description: API for 0x45 + license: + name: MIT + url: https://github.com/watzon/0x45/blob/main/LICENSE + title: 0x45 API + version: "1.0" +paths: + /api/keys/request: + post: + consumes: + - application/json + operationId: HandleRequestAPIKey + parameters: + - description: Request a new API key + in: body + name: request + required: true + schema: + $ref: '#/definitions/services.APIKeyRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/services.APIKeyResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/services.APIKeyResponse' + summary: Request a new API key + tags: + - API Key +swagger: "2.0" diff --git a/go.mod b/go.mod index 6b936c1..abf1f87 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,22 @@ require ( gorm.io/gorm v1.25.12 ) +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect + github.com/swaggo/swag v1.16.3 // indirect + golang.org/x/tools v0.27.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) + require ( github.com/alecthomas/chroma v0.10.0 github.com/andybalholm/brotli v1.1.1 // indirect @@ -51,6 +67,7 @@ require ( github.com/dlclark/regexp2 v1.11.4 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/gofiber/swagger v1.1.0 github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 04ca8f7..6865fc4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= @@ -50,6 +56,7 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -71,8 +78,20 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ= +github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8= github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= github.com/gofiber/template/handlebars/v2 v2.1.10 h1:Qc+uUMULCqW60LF4VKO1REpiyDAUy3vqW7xq66FPJGM= @@ -108,16 +127,25 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -129,6 +157,7 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -163,11 +192,16 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= @@ -190,30 +224,41 @@ golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+h golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go deleted file mode 100644 index 28654e7..0000000 --- a/internal/middleware/auth.go +++ /dev/null @@ -1,93 +0,0 @@ -package middleware - -import ( - "strings" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/watzon/0x45/internal/models" - "gorm.io/gorm" -) - -type AuthMiddleware struct { - db *gorm.DB -} - -func NewAuthMiddleware(db *gorm.DB) *AuthMiddleware { - return &AuthMiddleware{db: db} -} - -// Auth returns a middleware that validates API keys -func (m *AuthMiddleware) Auth(required bool) fiber.Handler { - return func(c *fiber.Ctx) error { - // First try to get API key from Authorization header - auth := c.Get("Authorization") - apiKey := "" - - if strings.HasPrefix(auth, "Bearer ") { - apiKey = strings.TrimPrefix(auth, "Bearer ") - } else { - // If not in header, try to get from query parameter - apiKey = c.Query("api_key") - } - - // If no API key found in either place - if apiKey == "" { - if required { - return fiber.NewError(fiber.StatusUnauthorized, "API key required") - } - return c.Next() - } - - // Validate API key and set rate limits - key, err := m.validateAPIKey(apiKey) - if err != nil { - if required { - return fiber.NewError(fiber.StatusUnauthorized, "Invalid API key") - } - return c.Next() - } - - // Check rate limit - if err := m.checkRateLimit(key); err != nil { - return fiber.NewError(fiber.StatusTooManyRequests, "Rate limit exceeded") - } - - // Store API key in context - c.Locals("apiKey", key) - return c.Next() - } -} - -func (m *AuthMiddleware) validateAPIKey(key string) (*models.APIKey, error) { - var apiKey models.APIKey - err := m.db.Where("key = ?", key).First(&apiKey).Error - if err != nil { - return nil, err - } - - // Update last used timestamp - m.db.Model(&apiKey).Updates(map[string]any{ - "last_used_at": time.Now(), - "usage_count": gorm.Expr("usage_count + 1"), - }) - - return &apiKey, nil -} - -func (m *AuthMiddleware) checkRateLimit(key *models.APIKey) error { - // Get usage count in the last hour - var count int64 - err := m.db.Model(&models.APIKey{}). - Where("key = ? AND last_used_at > ?", key.Key, time.Now().Add(-time.Hour)). - Count(&count).Error - if err != nil { - return err - } - - if count >= int64(key.RateLimit) { - return fiber.NewError(fiber.StatusTooManyRequests, "Rate limit exceeded") - } - - return nil -} diff --git a/internal/server/handlers/apikey.go b/internal/server/handlers/apikey.go index 1f740a4..c255279 100644 --- a/internal/server/handlers/apikey.go +++ b/internal/server/handlers/apikey.go @@ -21,12 +21,19 @@ func NewAPIKeyHandlers(services *services.Services, logger *zap.Logger, config * } } -// HandleRequestAPIKey handles the initial API key request +// @id HandleRequestAPIKey +// @Summary Request a new API key +// @Tags API Key +// @Accept json +// @Produce json +// @Param request body services.APIKeyRequest true "Request a new API key" +// @Success 200 {object} services.APIKeyResponse +// @Failure 400 {object} fiber.Error +// @Router /api/keys/request [post] func (h *APIKeyHandlers) HandleRequestAPIKey(c *fiber.Ctx) error { return h.services.APIKey.RequestKey(c) } -// HandleVerifyAPIKey verifies the email and activates the API key func (h *APIKeyHandlers) HandleVerifyAPIKey(c *fiber.Ctx) error { return h.services.APIKey.VerifyKey(c) } diff --git a/internal/server/middleware/ratelimit.go b/internal/server/middleware/ratelimit.go index ec3fc4c..848874b 100644 --- a/internal/server/middleware/ratelimit.go +++ b/internal/server/middleware/ratelimit.go @@ -1,7 +1,11 @@ package middleware import ( + "context" + "strings" + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" "github.com/watzon/0x45/internal/config" "github.com/watzon/0x45/internal/ratelimit" "go.uber.org/zap" @@ -35,7 +39,21 @@ func NewRateLimiter(logger *zap.Logger, config *config.Config) *RateLimiter { Burst: config.Server.RateLimit.PerIP.Burst, }, UseRedis: config.Redis.Enabled, - Redis: nil, // Will be set by server if Redis is enabled + } + + if config.Redis.Enabled { + redisClient := redis.NewClient(&redis.Options{ + Addr: config.Redis.Address, + Password: config.Redis.Password, + DB: config.Redis.DB, + }) + + // Test Redis connection + if _, err := redisClient.Ping(context.Background()).Result(); err != nil { + logger.Error("failed to connect to Redis", zap.Error(err)) + } + + limiterConfig.Redis = redisClient } return &RateLimiter{ @@ -48,6 +66,11 @@ func NewRateLimiter(logger *zap.Logger, config *config.Config) *RateLimiter { // RateLimit returns a middleware that limits requests func (m *RateLimiter) RateLimit() fiber.Handler { return func(c *fiber.Ctx) error { + // Skip rate limiting on non-API routes + if !strings.HasPrefix(c.Path(), "/api/") { + return c.Next() + } + // Skip rate limiting if request has a valid API key if c.Locals("apiKey") != nil { return c.Next() diff --git a/internal/server/server.go b/internal/server/server.go index 8ee1e58..85a15c7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -5,8 +5,9 @@ import ( "fmt" "github.com/gofiber/fiber/v2" + "github.com/gofiber/swagger" "github.com/gofiber/template/handlebars/v2" - "github.com/redis/go-redis/v9" + _ "github.com/watzon/0x45/docs" "github.com/watzon/0x45/internal/config" "github.com/watzon/0x45/internal/database" "github.com/watzon/0x45/internal/server/handlers" @@ -64,26 +65,6 @@ func New(db *database.Database, storageManager *storage.StorageManager, config * // Serve static files app.Static("/public", "./public") - // Initialize Redis if enabled - if config.Redis.Enabled { - redisClient := redis.NewClient(&redis.Options{ - Addr: config.Redis.Address, - Password: config.Redis.Password, - DB: config.Redis.DB, - }) - - // Test Redis connection - if _, err := redisClient.Ping(context.Background()).Result(); err != nil { - logger.Error("failed to connect to Redis", zap.Error(err)) - return nil - } - - // Set Redis client in rate limiter if using Redis - if config.Server.Prefork { - // TODO: Set Redis client in rate limiter - } - } - return &Server{ app: app, db: db, @@ -102,6 +83,7 @@ func (s *Server) SetupRoutes() { s.app.Get("/", s.handlers.Web.HandleIndex) s.app.Get("/stats", s.handlers.Web.HandleStats) s.app.Get("/docs", s.handlers.Web.HandleDocs) + s.app.Get("/api-docs/*", swagger.HandlerDefault) // API Key routes apiKeys := s.app.Group("/api/keys") diff --git a/internal/server/services/apikey.go b/internal/server/services/apikey.go index 31a3bb8..aaa692a 100644 --- a/internal/server/services/apikey.go +++ b/internal/server/services/apikey.go @@ -14,10 +14,24 @@ import ( ) type APIKeyService struct { - db *gorm.DB - logger *zap.Logger - config *config.Config - mailer *mailer.Mailer + db *gorm.DB + logger *zap.Logger + config *config.Config + mailer *mailer.Mailer +} + +type APIKeyRequest struct { + Email string `json:"email"` + Name string `json:"name"` +} + +type APIKeyResponse struct { + Message string `json:"message"` + Key string `json:"key"` +} + +type VerifyAPIKeyRequest struct { + Token string `json:"token"` } func NewAPIKeyService(db *gorm.DB, logger *zap.Logger, config *config.Config) *APIKeyService { @@ -27,19 +41,16 @@ func NewAPIKeyService(db *gorm.DB, logger *zap.Logger, config *config.Config) *A } return &APIKeyService{ - db: db, - logger: logger, - config: config, - mailer: m, + db: db, + logger: logger, + config: config, + mailer: m, } } // RequestKey handles the initial API key request func (s *APIKeyService) RequestKey(c *fiber.Ctx) error { - var req struct { - Email string `json:"email"` - Name string `json:"name"` - } + var req APIKeyRequest if err := c.BodyParser(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") @@ -119,10 +130,10 @@ func (s *APIKeyService) VerifyKey(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify API key") } - return c.JSON(fiber.Map{ - "message": "API key verified successfully", - "key": apiKey.Key, - }) + return c.Render("verify_success", fiber.Map{ + "baseUrl": s.config.Server.BaseURL, + "apiKey": apiKey.Key, + }, "layouts/main") } // Helper functions diff --git a/cmd/server/main.go b/main.go similarity index 88% rename from cmd/server/main.go rename to main.go index 8ccc871..4ae2f60 100644 --- a/cmd/server/main.go +++ b/main.go @@ -12,6 +12,13 @@ import ( "github.com/watzon/0x45/internal/storage" ) +// @title 0x45 API +// @version 1.0 +// @description API for 0x45 +// @license.name MIT +// @license.url https://github.com/watzon/0x45/blob/main/LICENSE +// @host localhost:3000 +// @BasePath / func main() { // Load config cfg, err := config.Load()