-
Notifications
You must be signed in to change notification settings - Fork 3
/
router.go
295 lines (258 loc) · 10.1 KB
/
router.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package router
import (
"net/http"
"net/url"
"os"
"strings"
docs "github.com/envelope-zero/backend/v2/api"
"github.com/envelope-zero/backend/v2/pkg/controllers"
"github.com/envelope-zero/backend/v2/pkg/database"
"github.com/envelope-zero/backend/v2/pkg/httperrors"
"github.com/envelope-zero/backend/v2/pkg/httputil"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/logger"
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/requestid"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// This is set at build time, see Makefile.
var version = "0.0.0"
func Config(url *url.URL) (*gin.Engine, error) {
// Set up the router and middlewares
r := gin.New()
// Don’t process X-Forwarded-For header as we do not do anything with
// client IPs
r.ForwardedByClientIP = false
// Send a HTTP 405 (Method not allowed) for all paths where there is
// a handler, but not for the specific method used
r.HandleMethodNotAllowed = true
r.Use(gin.Recovery())
r.Use(requestid.New())
r.Use(URLMiddleware(url))
r.NoMethod(func(c *gin.Context) {
httperrors.New(c, http.StatusMethodNotAllowed, "This HTTP method is not allowed for the endpoint you called")
})
r.Use(logger.SetLogger(
logger.WithDefaultLevel(zerolog.InfoLevel),
logger.WithClientErrorLevel(zerolog.InfoLevel),
logger.WithServerErrorLevel(zerolog.ErrorLevel),
logger.WithLogger(func(c *gin.Context, logger zerolog.Logger) zerolog.Logger {
return logger.With().
Str("request-id", requestid.Get(c)).
Str("method", c.Request.Method).
Str("path", c.Request.URL.Path).
Int("status", c.Writer.Status()).
Int("size", c.Writer.Size()).
Str("user-agent", c.Request.UserAgent()).
Logger()
})))
// CORS settings
allowOrigins, ok := os.LookupEnv("CORS_ALLOW_ORIGINS")
if ok {
log.Debug().Str("CORS Allowed Origins", allowOrigins).Msg("Router")
r.Use(cors.New(cors.Config{
AllowOrigins: strings.Fields(allowOrigins),
AllowMethods: []string{"OPTIONS", "GET", "POST", "PATCH", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"},
AllowCredentials: true,
}))
}
// Disable the gin debug route printing as it clutters logs (and test logs)
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, numHandlers int) {}
// Don’t trust any proxy. We do not process any client IPs,
// therefore we don’t need to trust anyone here.
_ = r.SetTrustedProxies([]string{})
log.Debug().Str("API Base URL", url.String()).Str("Host", url.Host).Str("Path", url.Path).Msg("Router")
log.Info().Str("version", version).Msg("Router")
docs.SwaggerInfo.Host = url.Host
docs.SwaggerInfo.BasePath = url.Path
docs.SwaggerInfo.Title = "Envelope Zero"
docs.SwaggerInfo.Version = version
docs.SwaggerInfo.Description = "The backend for Envelope Zero, a zero based envelope budgeting solution. Check out the source code at https://github.com/envelope-zero/backend."
return r, nil
}
// AttachRoutes attaches the API routes to the router group that is passed in
// Separating this from RouterConfig() allows us to attach it to different
// paths for different use cases, e.g. the standalone version.
func AttachRoutes(co controllers.Controller, group *gin.RouterGroup) {
group.GET("", GetRoot)
group.OPTIONS("", OptionsRoot)
group.GET("/version", GetVersion)
group.OPTIONS("/version", OptionsVersion)
// pprof performance profiles
enablePprof, ok := os.LookupEnv("ENABLE_PPROF")
if ok && enablePprof == "true" {
pprof.RouteRegister(group, "debug/pprof")
}
group.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// API v1 setup
v1 := group.Group("/v1")
{
v1.GET("", GetV1)
v1.DELETE("", co.DeleteAll)
v1.OPTIONS("", OptionsV1)
}
co.RegisterBudgetRoutes(v1.Group("/budgets"))
co.RegisterAccountRoutes(v1.Group("/accounts"))
co.RegisterTransactionRoutes(v1.Group("/transactions"))
co.RegisterCategoryRoutes(v1.Group("/categories"))
co.RegisterEnvelopeRoutes(v1.Group("/envelopes"))
co.RegisterAllocationRoutes(v1.Group("/allocations"))
co.RegisterMonthRoutes(v1.Group("/months"))
co.RegisterImportRoutes(v1.Group("/import"))
co.RegisterMonthConfigRoutes(v1.Group("/month-configs"))
// API v2 setup
v2 := group.Group("/v2")
{
v2.GET("", GetV2)
v2.OPTIONS("", OptionsV2)
}
co.RegisterTransactionRoutesV2(v2.Group("/transactions"))
co.RegisterRenameRuleRoutes(v2.Group("/rename-rules"))
}
type RootResponse struct {
Links RootLinks `json:"links"`
}
type RootLinks struct {
Docs string `json:"docs" example:"https://example.com/api/docs/index.html"` // Swagger API documentation
Version string `json:"version" example:"https://example.com/api/version"` // Endpoint returning the version of the backend
V1 string `json:"v1" example:"https://example.com/api/v1"` // List endpoint for all v1 endpoints
V2 string `json:"v2" example:"https://example.com/api/v2"` // List endpoint for all v2 endpoints
}
// GetRoot returns the link list for the API root
//
// @Summary API root
// @Description Entrypoint for the API, listing all endpoints
// @Tags General
// @Success 200 {object} RootResponse
// @Router / [get]
func GetRoot(c *gin.Context) {
c.JSON(http.StatusOK, RootResponse{
Links: RootLinks{
Docs: c.GetString(string(database.ContextURL)) + "/docs/index.html",
Version: c.GetString(string(database.ContextURL)) + "/version",
V1: c.GetString(string(database.ContextURL)) + "/v1",
V2: c.GetString(string(database.ContextURL)) + "/v2",
},
})
}
type VersionResponse struct {
Data VersionObject `json:"data"` // Data object for the version endpoint
}
type VersionObject struct {
Version string `json:"version" example:"1.1.0"` // the running version of the Envelope Zero backend
}
// GetVersion returns the API version object
//
// @Summary API version
// @Description Returns the software version of the API
// @Tags General
// @Success 200 {object} VersionResponse
// @Router /version [get]
func GetVersion(c *gin.Context) {
c.JSON(http.StatusOK, VersionResponse{
Data: VersionObject{
Version: version,
},
})
}
// OptionsRoot returns the allowed HTTP methods
//
// @Summary Allowed HTTP verbs
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
// @Tags General
// @Success 204
// @Router / [options]
func OptionsRoot(c *gin.Context) {
httputil.OptionsGet(c)
}
// OptionsVersion returns the allowed HTTP methods
//
// @Summary Allowed HTTP verbs
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
// @Tags General
// @Success 204
// @Router /version [options]
func OptionsVersion(c *gin.Context) {
httputil.OptionsGet(c)
}
type V1Response struct {
Links V1Links `json:"links"` // Links for the v1 API
}
type V1Links struct {
Budgets string `json:"budgets" example:"https://example.com/api/v1/budgets"` // URL of budget list endpoint
Accounts string `json:"accounts" example:"https://example.com/api/v1/accounts"` // URL of account list endpoint
Categories string `json:"categories" example:"https://example.com/api/v1/categories"` // URL of category list endpoint
Transactions string `json:"transactions" example:"https://example.com/api/v1/transactions"` // URL of transaction list endpoint
Envelopes string `json:"envelopes" example:"https://example.com/api/v1/envelopes"` // URL of envelope list endpoint
Allocations string `json:"allocations" example:"https://example.com/api/v1/allocations"` // URL of allocation list endpoint
Months string `json:"months" example:"https://example.com/api/v1/months"` // URL of month list endpoint
Import string `json:"import" example:"https://example.com/api/v1/import"` // URL of import list endpoint
}
// GetV1 returns the link list for v1
//
// @Summary v1 API
// @Description Returns general information about the v1 API
// @Tags v1
// @Success 200 {object} V1Response
// @Router /v1 [get]
func GetV1(c *gin.Context) {
c.JSON(http.StatusOK, V1Response{
Links: V1Links{
Budgets: c.GetString(string(database.ContextURL)) + "/v1/budgets",
Accounts: c.GetString(string(database.ContextURL)) + "/v1/accounts",
Categories: c.GetString(string(database.ContextURL)) + "/v1/categories",
Transactions: c.GetString(string(database.ContextURL)) + "/v1/transactions",
Envelopes: c.GetString(string(database.ContextURL)) + "/v1/envelopes",
Allocations: c.GetString(string(database.ContextURL)) + "/v1/allocations",
Months: c.GetString(string(database.ContextURL)) + "/v1/months",
Import: c.GetString(string(database.ContextURL)) + "/v1/import",
},
})
}
// OptionsV1 returns the allowed HTTP methods
//
// @Summary Allowed HTTP verbs
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
// @Tags v1
// @Success 204
// @Router /v1 [options]
func OptionsV1(c *gin.Context) {
httputil.OptionsGetDelete(c)
}
type V2Response struct {
Links V2Links `json:"links"` // Links for the v2 API
}
type V2Links struct {
Transactions string `json:"transactions" example:"https://example.com/api/v2/transactions"` // URL of transaction list endpoint
RenameRules string `json:"rename-rules" example:"https://example.com/api/v2/rename-rules"` // URL of rename-rule list endpoint
}
// GetV2 returns the link list for v2
//
// @Summary v2 API
// @Description Returns general information about the v2 API
// @Tags v2
// @Success 200 {object} V2Response
// @Router /v2 [get]
func GetV2(c *gin.Context) {
c.JSON(http.StatusOK, V2Response{
Links: V2Links{
Transactions: c.GetString(string(database.ContextURL)) + "/v2/transactions",
RenameRules: c.GetString(string(database.ContextURL)) + "/v2/rename-rules",
},
})
}
// OptionsV2 returns the allowed HTTP methods
//
// @Summary Allowed HTTP verbs
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
// @Tags v2
// @Success 204
// @Router /v2 [options]
func OptionsV2(c *gin.Context) {
httputil.OptionsGet(c)
}