From 3e1aa373ae108bba6552cf9b38c62b1908c4edf6 Mon Sep 17 00:00:00 2001 From: Ahmad Habibi Date: Sun, 7 Jan 2024 11:33:05 +0800 Subject: [PATCH 1/5] add rate limiter in web server --- W/engine.go | 21 ++++++++++++++++++++- go.mod | 1 + go.sum | 14 ++++++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/W/engine.go b/W/engine.go index 8cb7951..f004043 100644 --- a/W/engine.go +++ b/W/engine.go @@ -18,6 +18,9 @@ import ( "github.com/tdewolff/minify" "github.com/tdewolff/minify/css" "github.com/tdewolff/minify/js" + limiter "github.com/ulule/limiter/v3" + mfasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp" + "github.com/ulule/limiter/v3/drivers/store/memory" "github.com/valyala/fasthttp" ) @@ -173,11 +176,27 @@ func (engine *Engine) MinifyAssets() { // start the server func (engine *Engine) StartServer(addressPort string) { + // periods: + // * "S": second + // * "M": minute + // * "H": hour + // * "D": day + // + // * 300 reqs/minute: "300-M" + rate, errLimiter := limiter.NewRateFromFormatted("300-M") + L.IsError(errLimiter, `Failed to add rate limiter `) + limitStore := memory.NewStore() + instance := limiter.New( + limitStore, + rate, + limiter.WithTrustForwardHeader(true), + ) + middleware := mfasthttp.NewMiddleware(instance) engine.MinifyAssets() L.LOG.Notice(engine.Name + ` ` + S.IfElse(engine.DebugMode, `[DEVELOPMENT]`, `[PRODUCTION]`) + ` server with ` + I.ToStr(len(Routes)) + ` route(s) on ` + addressPort + "\n Ajax Error Log: " + engine.LogPath + "\n Work Directory: " + engine.BaseDir) - err := fasthttp.ListenAndServe(addressPort, engine.Router.Handler) + err := fasthttp.ListenAndServe(addressPort, middleware.Handle(engine.Router.Handler)) L.IsError(err, `Failed to listen on `+addressPort) engine.Logger.Close() } diff --git a/go.mod b/go.mod index e4b30f9..e52edce 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/tarantool/go-tarantool v1.12.1 github.com/tdewolff/minify v2.3.6+incompatible + github.com/ulule/limiter/v3 v3.11.2 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.51.0 github.com/vburenin/nsync v0.0.0-20160822015540-9a75d1c80410 diff --git a/go.sum b/go.sum index 8b2bbae..4c8c163 100644 --- a/go.sum +++ b/go.sum @@ -143,12 +143,15 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -314,8 +317,9 @@ github.com/kr/pty v1.1.3/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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -492,6 +496,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA= +github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= 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.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= From 66bd741a6ae702b11db1fdd935ea16a4c29c37ab Mon Sep 17 00:00:00 2001 From: Ahmad Habibi Date: Sun, 7 Jan 2024 11:46:57 +0800 Subject: [PATCH 2/5] fix error handling --- W/engine.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/W/engine.go b/W/engine.go index f004043..879df22 100644 --- a/W/engine.go +++ b/W/engine.go @@ -184,7 +184,11 @@ func (engine *Engine) StartServer(addressPort string) { // // * 300 reqs/minute: "300-M" rate, errLimiter := limiter.NewRateFromFormatted("300-M") - L.IsError(errLimiter, `Failed to add rate limiter `) + var isErrLimiter bool + if errLimiter != nil { + L.IsError(errLimiter, `Failed to add rate limiter`) + isErrLimiter = true + } limitStore := memory.NewStore() instance := limiter.New( limitStore, @@ -196,7 +200,14 @@ func (engine *Engine) StartServer(addressPort string) { L.LOG.Notice(engine.Name + ` ` + S.IfElse(engine.DebugMode, `[DEVELOPMENT]`, `[PRODUCTION]`) + ` server with ` + I.ToStr(len(Routes)) + ` route(s) on ` + addressPort + "\n Ajax Error Log: " + engine.LogPath + "\n Work Directory: " + engine.BaseDir) - err := fasthttp.ListenAndServe(addressPort, middleware.Handle(engine.Router.Handler)) + + var err error + if isErrLimiter { + // not using rate limiter + err = fasthttp.ListenAndServe(addressPort, engine.Router.Handler) + } else { + err = fasthttp.ListenAndServe(addressPort, middleware.Handle(engine.Router.Handler)) + } L.IsError(err, `Failed to listen on `+addressPort) engine.Logger.Close() } From 19310f003d71a9041e617fe84b80c8829742029c Mon Sep 17 00:00:00 2001 From: Ahmad Habibi Date: Tue, 27 Feb 2024 19:21:49 +0800 Subject: [PATCH 3/5] swagger generator, swagger UI --- W2/internal/example2/openapi-conf.json | 23 + .../example2/presentation/1_codegen_test.go | 198 +++++- .../example2/svelte/apidocs/_layout.html | 24 + .../example2/svelte/apidocs/index.html | 617 ++++++++++++++++++ .../example2/svelte/apidocs/index.svelte | 0 5 files changed, 853 insertions(+), 9 deletions(-) create mode 100644 W2/internal/example2/openapi-conf.json create mode 100644 W2/internal/example2/svelte/apidocs/_layout.html create mode 100644 W2/internal/example2/svelte/apidocs/index.html create mode 100644 W2/internal/example2/svelte/apidocs/index.svelte diff --git a/W2/internal/example2/openapi-conf.json b/W2/internal/example2/openapi-conf.json new file mode 100644 index 0000000..547439f --- /dev/null +++ b/W2/internal/example2/openapi-conf.json @@ -0,0 +1,23 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Example2", + "description": "API Spec for Example2", + "termsOfService": "https://example2.com/term-of-service", + "contact": { + "name": "API Support", + "email": "support@example2.com" + }, + "license": { + "name": "MIT License", + "url": "https://en.wikipedia.org/wiki/MIT_License" + }, + "version": "1.0.0" + }, + "servers": [ + { + "url": "https://example2.com", + "description": "Example2 Server" + } + ] +} \ No newline at end of file diff --git a/W2/internal/example2/presentation/1_codegen_test.go b/W2/internal/example2/presentation/1_codegen_test.go index 19d3065..9b9a58e 100644 --- a/W2/internal/example2/presentation/1_codegen_test.go +++ b/W2/internal/example2/presentation/1_codegen_test.go @@ -9,10 +9,13 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "time" + "github.com/goccy/go-json" "github.com/kokizzu/gotro/L" + "github.com/kokizzu/gotro/M" "github.com/kokizzu/gotro/S" ) @@ -33,6 +36,7 @@ func BenchmarkGenerateViews(b *testing.B) { JsApiGenFile: "../svelte/jsApi.GEN.js", CmdRunGenFile: "./cmd_run.GEN.go", WebViewGenFile: "./web_view.GEN.go", + SwaggerGenFile: "../svelte/swagger.json", } p.StartCodegen() @@ -43,6 +47,10 @@ const NL = "\n" const TAB = "\t" const generatedComment = NL + `// Code generated by 1_codegen_test.go DO NOT EDIT.` + NL +func tabs(total int) string { + return S.Repeat(TAB, total) +} + type codegen struct { ModelDir string // ../model models models @@ -58,6 +66,7 @@ type codegen struct { CmdRunGenFile string // cmd_run.GEN.go JsApiGenFile string // jsApi.GEN.js WebViewGenFile string // web_view.GEN.go + SwaggerGenFile string // swagger.json ProjectName string // based on go.mod } @@ -348,6 +357,7 @@ func (c *codegen) StartCodegen() { c.GenerateApiRoutesFile() c.GenerateJsApiFile() c.GenerateCmdRunFile() + c.GenerateSwaggerFile() // parse svelte files start = time.Now() @@ -362,6 +372,7 @@ func (c *codegen) StartCodegen() { } return nil }) + L.IsError(err, `filepath.Walk`) L.TimeTrack(start, `parsing svelte dir`) c.GenerateWebRouteFile() @@ -377,7 +388,7 @@ func (d *domains) parseDomainFile(path string) { ast.Walk(d, p) } -func (d *domains) eachSortedHandler(eachFunc func(name string, handler tmethod)) { +func (d *domains) eachSortedHandler(eachFunc func(name string, handler tmethod, isEnd bool)) { // sort handlers handlers := d.handlers.byRcvDotMethod handlerNames := make([]string, 0, len(handlers)) @@ -393,8 +404,12 @@ func (d *domains) eachSortedHandler(eachFunc func(name string, handler tmethod)) } sort.Strings(handlerNames) - for _, name := range handlerNames { - eachFunc(name, byName[name]) + for idx, name := range handlerNames { + end := false + if idx == len(handlerNames)-1 { + end = true + } + eachFunc(name, byName[name], end) } } @@ -410,7 +425,7 @@ import ( var allCommands = []string{ `) - c.domains.eachSortedHandler(func(name string, handler tmethod) { + c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) { b.WriteString(TAB + `domain.` + name + `Action,` + NL) }) b.WriteString(`}` + NL) @@ -428,12 +443,12 @@ func (c *codegen) GenerateApiRoutesFile() { import ( "github.com/gofiber/fiber/v2" - "example2/domain" + "benakun/domain" ) func ApiRoutes(fw *fiber.App, d *domain.Domain) { `) - c.domains.eachSortedHandler(func(name string, handler tmethod) { + c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) { b.WriteString(` // ` + name + ` fw.Post("/"+domain.` + name + `Action, func(c *fiber.Ctx) error { @@ -482,7 +497,7 @@ function wrapOk( cb ) { `) b.WriteString(generatedComment) - c.domains.eachSortedHandler(func(name string, handler tmethod) { + c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) { fields := c.domains.types.byName[handler.In].fields c.jsObject(&b, name+`In`, fields, 0) @@ -601,14 +616,14 @@ func (c *codegen) GenerateCmdRunFile() { import ( "os" - "example2/domain" + "benakun/domain" ) ` + generatedComment + ` func cmdRun(b *domain.Domain, action string, payload []byte) { switch action {`) - c.domains.eachSortedHandler(func(name string, handler tmethod) { + c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) { b.WriteString(` case domain.` + name + `Action: in := domain.` + name + `In{} @@ -668,3 +683,168 @@ func (v *Views) Render` + cacheName + `(c *fiber.Ctx, m M.SX) error { L.CreateFile(c.WebViewGenFile, b.String()) } + +func (c *codegen) GenerateSwaggerFile() { + defer L.TimeTrack(time.Now(), `GenerateSwaggerFile`) + + confJson, err := os.ReadFile("../openapi-conf.json") + L.PanicIf(err, `error read OpenAPI config file`) + + var jsonMap M.SX + err = json.Unmarshal(confJson, &jsonMap) + L.PanicIf(err, `error unmarshall OpenAPI config`) + + jsonString, err := json.MarshalIndent(jsonMap, "", " ") + L.PanicIf(err, `marshall indent`) + + jsonString = jsonString[1 : len(jsonString)-1] + newJsonString := string(jsonString) + lines := strings.Split(newJsonString, NL) + lines = lines[:len(lines)-1] + + newJsonString = strings.Join(lines, NL) + + b := bytes.Buffer{} + + b.WriteString(`{` + newJsonString + `, + "paths": { +`) + + c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) { + coma := `,` + if isEnd { + coma = `` + } + for _, a := range allCommands { + parts := strings.Split(a, "/") + for i, part := range parts { + parts[i] = strings.Title(part) + } + newName := strings.Join(parts, "") + if name == newName { + b.WriteString(TAB + TAB + `"/` + a + `": {` + NL) + } + } + b.WriteString(TAB + TAB + TAB + `"post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { +`) + + fieldsIn := c.domains.types.byName[handler.In].fields + c.swaggerRequest(&b, name+`In`, fieldsIn, 0) + b.WriteString(tabs(8) + `} + } + } + } + },`) + b.WriteString(` + "responses": { + "200": { + "description": "` + name + `", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { +`) + fields := c.domains.types.byName[handler.Out].fields + c.swaggerResponses(&b, name+`Out`, fields, 2) + b.WriteString(TAB + TAB + TAB + TAB + TAB + TAB + TAB + TAB + TAB + `} + } + } + } + } + } + } + }` + coma + NL) + }) + + b.WriteString(TAB + `}, + "components": { + "securitySchemes": { + "CookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "auth", + "description": "Authentication for Benakun" + } + } + } +} +`) + + L.CreateFile(c.SwaggerGenFile, b.String()) +} + +func (c *codegen) swaggerFields(content *bytes.Buffer, fields []tfield, indent int) { + for idx, field := range fields { + end := false + if idx == len(fields)-1 { + end = true + } + c.swaggerField(content, field, indent, end) + } +} + +func (c *codegen) swaggerField(b *bytes.Buffer, field tfield, indent int, isEnd bool) { + coma := `,` + if isEnd { + coma = `` + } + t := field.Type + // skip unecessary fields + switch t { + case `Tt.Adapter`, `Ch.Adapter`: + return + } + + spaces := S.Repeat(TAB, indent*2) + b.WriteString(tabs(6) + spaces + `"` + S.CamelCase(field.Name) + `": `) + + switch t { + case `int`, `uint8`, `uint16`, `uint32`, `uint64`, `int8`, `int16`, `int32`, `int64`, `float32`, `float64`: + b.WriteString(`{ +` + tabs(8) + spaces + `"type": "number" +` + tabs(6) + spaces + `}` + coma) + case `string`: + b.WriteString(`{ +` + tabs(8) + spaces + `"type": "string" +` + tabs(6) + spaces + `}` + coma) + case `bool`: + b.WriteString(`{ +` + tabs(6) + TAB + spaces + `"type": "boolean" +` + tabs(6) + spaces + `}` + coma) + case `[]string`: + b.WriteString(`{ +` + tabs(8) + spaces + `"type": "array", +` + tabs(9) + spaces + `"items": { +` + tabs(10) + spaces + `"type": "string" +` + tabs(9) + spaces + `} +` + tabs(8) + spaces + `}` + coma) + default: + ty := c.models.types.byName[t] + b.WriteString(`{ +` + tabs(6) + spaces + `"type": "object", +` + tabs(6) + spaces + `"properties": {` + NL) + c.swaggerFields(b, ty.fields, indent+1) + b.WriteString(tabs(8) + spaces + `} +` + tabs(6) + spaces + `}` + coma) + } + b.WriteString(NL) +} + +func (c *codegen) swaggerResponses(b *bytes.Buffer, name string, fields []tfield, indent int) { + c.swaggerFields(b, fields, indent) +} + +func (c *codegen) swaggerRequest(b *bytes.Buffer, name string, fields []tfield, indent int) { + c.swaggerFields(b, fields, indent) +} diff --git a/W2/internal/example2/svelte/apidocs/_layout.html b/W2/internal/example2/svelte/apidocs/_layout.html new file mode 100644 index 0000000..28aa919 --- /dev/null +++ b/W2/internal/example2/svelte/apidocs/_layout.html @@ -0,0 +1,24 @@ + + + + + + + + /*! title */ + + + +
+ + + + + \ No newline at end of file diff --git a/W2/internal/example2/svelte/apidocs/index.html b/W2/internal/example2/svelte/apidocs/index.html new file mode 100644 index 0000000..3b8c57a --- /dev/null +++ b/W2/internal/example2/svelte/apidocs/index.html @@ -0,0 +1,617 @@ + + + + + + /*! title */ + + + +
+ + +
+ + + + + + + \ No newline at end of file diff --git a/W2/internal/example2/svelte/apidocs/index.svelte b/W2/internal/example2/svelte/apidocs/index.svelte new file mode 100644 index 0000000..e69de29 From c1bea25afe3d55b277a30b0ba64b45ef928626e5 Mon Sep 17 00:00:00 2001 From: Ahmad Habibi Date: Tue, 27 Feb 2024 19:26:38 +0800 Subject: [PATCH 4/5] fix generator --- .../example2/presentation/1_codegen_test.go | 6 +- .../example2/presentation/web_static.go | 7 + .../example2/presentation/web_view.GEN.go | 6 + W2/internal/example2/svelte/swagger.json | 1376 +++++++++++++++++ 4 files changed, 1392 insertions(+), 3 deletions(-) create mode 100644 W2/internal/example2/svelte/swagger.json diff --git a/W2/internal/example2/presentation/1_codegen_test.go b/W2/internal/example2/presentation/1_codegen_test.go index 9b9a58e..ffcd390 100644 --- a/W2/internal/example2/presentation/1_codegen_test.go +++ b/W2/internal/example2/presentation/1_codegen_test.go @@ -443,7 +443,7 @@ func (c *codegen) GenerateApiRoutesFile() { import ( "github.com/gofiber/fiber/v2" - "benakun/domain" + "example2/domain" ) func ApiRoutes(fw *fiber.App, d *domain.Domain) { @@ -616,7 +616,7 @@ func (c *codegen) GenerateCmdRunFile() { import ( "os" - "benakun/domain" + "example2/domain" ) ` + generatedComment + ` @@ -774,7 +774,7 @@ func (c *codegen) GenerateSwaggerFile() { "type": "apiKey", "in": "cookie", "name": "auth", - "description": "Authentication for Benakun" + "description": "Authentication for example2" } } } diff --git a/W2/internal/example2/presentation/web_static.go b/W2/internal/example2/presentation/web_static.go index 5c98ce9..d42eea6 100644 --- a/W2/internal/example2/presentation/web_static.go +++ b/W2/internal/example2/presentation/web_static.go @@ -73,6 +73,13 @@ func (w *WebServer) WebStatic(fw *fiber.App, d *domain.Domain) { }) }) + fw.Get(`/apidocs`, func(ctx *fiber.Ctx) error { + return views.RenderApidocsIndex(ctx, M.SX{ + `title`: `Example2 API Docs`, + `description`: `Restful API Documentation of Example2`, + }) + }) + } func userInfoFromContext(c *fiber.Ctx, d *domain.Domain) (domain.UserProfileIn, *rqAuth.Users, M.SB) { diff --git a/W2/internal/example2/presentation/web_view.GEN.go b/W2/internal/example2/presentation/web_view.GEN.go index 2632075..2f863c7 100644 --- a/W2/internal/example2/presentation/web_view.GEN.go +++ b/W2/internal/example2/presentation/web_view.GEN.go @@ -10,12 +10,18 @@ import ( var viewList = map[string]string{ + `ApidocsIndex`: `../svelte/apidocs/index.html`, // ../svelte/apidocs/index.svelte `Index`: `../svelte/index.html`, // ../svelte/index.svelte `SuperAdminDashboard`: `../svelte/superAdmin/dashboard.html`, // ../svelte/superAdmin/dashboard.svelte `SuperAdminUserManagement`: `../svelte/superAdmin/userManagement.html`, // ../svelte/superAdmin/userManagement.svelte } +func (v *Views) RenderApidocsIndex(c *fiber.Ctx, m M.SX) error { + c.Set("Content-Type", "text/html; charset=utf-8") + return c.SendString(v.cache[`ApidocsIndex`].Str(m)) +} + func (v *Views) RenderIndex(c *fiber.Ctx, m M.SX) error { c.Set("Content-Type", "text/html; charset=utf-8") return c.SendString(v.cache[`Index`].Str(m)) diff --git a/W2/internal/example2/svelte/swagger.json b/W2/internal/example2/svelte/swagger.json new file mode 100644 index 0000000..ba7c52d --- /dev/null +++ b/W2/internal/example2/svelte/swagger.json @@ -0,0 +1,1376 @@ +{ + "info": { + "contact": { + "email": "support@example2.com", + "name": "API Support" + }, + "description": "API Spec for Example2", + "license": { + "name": "MIT License", + "url": "https://en.wikipedia.org/wiki/MIT_License" + }, + "termsOfService": "https://example2.com/term-of-service", + "title": "Example2", + "version": "1.0.0" + }, + "openapi": "3.0.3", + "servers": [ + { + "description": "Example2 Server", + "url": "https://example2.com" + } + ], + "paths": { + "/guest/autoLogin": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uid": { + "type": "string" + }, + "token": { + "type": "string" + }, + "path": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestAutoLogin", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "segments": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + }, + "/guest/debug": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestDebug", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "request": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + }, + "/guest/externalAuth": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "provider": { + "type": "string" + }, + "redirect": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestExternalAuth", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "link": { + "type": "string" + }, + "clientID": { + "type": "string" + }, + "redirectUrl": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "csrfState": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/guest/forgotPassword": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestForgotPassword", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "resetPassUrl": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/guest/login": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestLogin", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "segments": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + }, + "/guest/oauthCallback": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "state": { + "type": "string" + }, + "code": { + "type": "string" + }, + "accessToken": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestOauthCallback", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "oauthUser": { + "type": "object", + "properties": { + } + }, + "email": { + "type": "string" + }, + "currentUser": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "provider": { + "type": "string" + }, + "segments": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + }, + "/guest/register": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestRegister", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "verifyEmailUrl": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/guest/resendVerificationEmail": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestResendVerificationEmail", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "verifyEmailUrl": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/guest/resetPassword": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretCode": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestResetPassword", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + } + } + } + } + } + } + } + } + }, + "/guest/verifyEmail": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secretCode": { + "type": "string" + }, + "hash": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "GuestVerifyEmail", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "email": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/superAdmin/dashboard": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + } + } + } + } + }, + "responses": { + "200": { + "description": "SuperAdminDashboard", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + }, + "/superAdmin/userManagement": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cmd": { + "type": "string" + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "withMeta": { + "type": "boolean" + }, + "pager": { + "type": "object", + "properties": { + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + }, + "filters": { + "type": "object", + "properties": { + } + }, + "order": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "SuperAdminUserManagement", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pager": { + "type": "object", + "properties": { + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + }, + "pages": { + "type": "number" + }, + "total": { + "type": "number" + }, + "filters": { + "type": "object", + "properties": { + } + }, + "order": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "meta": { + "type": "object", + "properties": { + "fields": { + "type": "object", + "properties": { + } + }, + "mutex": { + "type": "object", + "properties": { + } + }, + "cachedSelect": { + "type": "string" + } + } + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "users": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + }, + "/user/autoLoginLink": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "UserAutoLoginLink", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "link": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/user/changePassword": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "oldPass": { + "type": "string" + }, + "newPass": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "UserChangePassword", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + } + } + } + } + } + } + } + } + }, + "/user/logout": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + } + } + } + } + }, + "responses": { + "200": { + "description": "UserLogout", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "logoutAt": { + "type": "number" + } + } + } + } + } + } + } + } + }, + "/user/profile": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + } + } + } + } + }, + "responses": { + "200": { + "description": "UserProfile", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "segments": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + }, + "/user/sessionKill": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionTokenHash": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "UserSessionKill", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "logoutAt": { + "type": "number" + }, + "sessionTerminated": { + "type": "number" + } + } + } + } + } + } + } + } + }, + "/user/sessionsActive": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + } + } + } + } + }, + "responses": { + "200": { + "description": "UserSessionsActive", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionsActive": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + }, + "/user/updateProfile": { + "post": { + "security": [{ + "CookieAuth": [] + }], + "tags": ["API"], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userName": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "country": { + "type": "string" + }, + "language": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "UserUpdateProfile", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "number" + }, + "createdBy": { + "type": "number" + }, + "updatedAt": { + "type": "number" + }, + "updatedBy": { + "type": "number" + }, + "deletedAt": { + "type": "number" + }, + "passwordSetAt": { + "type": "number" + }, + "secretCode": { + "type": "string" + }, + "secretCodeAt": { + "type": "number" + }, + "verificationSentAt": { + "type": "number" + }, + "verifiedAt": { + "type": "number" + }, + "lastLoginAt": { + "type": "number" + }, + "fullName": { + "type": "string" + }, + "tenantCode": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "segments": { + "type": "object", + "properties": { + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "CookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "auth", + "description": "Authentication for example2" + } + } + } +} From 477c6d45004f21b4a2ba2d4967bd89fe14b57853 Mon Sep 17 00:00:00 2001 From: Ahmad Habibi Date: Tue, 27 Feb 2024 19:46:31 +0800 Subject: [PATCH 5/5] configurable rate limiter --- W/engine.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/W/engine.go b/W/engine.go index 879df22..c03ac24 100644 --- a/W/engine.go +++ b/W/engine.go @@ -49,6 +49,15 @@ type Engine struct { CreatedAt time.Time // assets