From 7481cec0d59a6ee6c745f01ec8db2a259a545086 Mon Sep 17 00:00:00 2001 From: Angel Beltran Date: Tue, 13 Jan 2026 22:07:20 -0700 Subject: [PATCH] installed gin --- ...listings_per_store_per_sync_group.down.sql | 2 + ...e_listings_per_store_per_sync_group.up.sql | 2 + go.mod | 21 + go.sum | 70 +- internal/domains/accounts/sync_groups.go | 69 +- internal/domains/authentication/auth.go | 2 +- internal/server/api/accounts/router.go | 84 +-- internal/server/api/auth/router.go | 52 +- internal/server/api/templates/router.go | 90 ++- internal/server/api/webhooks/etsy/webhooks.go | 81 ++- .../server/api/webhooks/tiktok/webhooks.go | 26 +- internal/server/api/webhooks/webhooks.go | 51 +- internal/server/api/webhooks/wix/webhooks.go | 26 +- internal/server/middleware/auth.go | 166 +++-- internal/server/middleware/log.go | 7 +- internal/server/response/handler.go | 14 +- internal/server/response/write.go | 12 +- internal/server/router/mux.go | 659 ------------------ internal/server/server.go | 98 +-- main.go | 56 +- .../draft/listings/{orderIndex}.html.tmpl | 6 +- .../inventory/sync-table-v2.html.tmpl | 4 +- templates/component_bodies/nav_bar.html.tmpl | 12 +- templates/layout.html.tmpl | 12 +- .../accounts/{acctID}/index.html.tmpl | 2 +- .../accounts/{acctID}/inventory.html.tmpl | 6 +- templates/page_bodies/conflict.html.tmpl | 2 +- templates/page_bodies/index.html.tmpl | 4 +- templates/page_bodies/not-found.html.tmpl | 2 +- 29 files changed, 544 insertions(+), 1094 deletions(-) create mode 100644 database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.down.sql create mode 100644 database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.up.sql delete mode 100644 internal/server/router/mux.go diff --git a/database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.down.sql b/database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.down.sql new file mode 100644 index 0000000..a02e79a --- /dev/null +++ b/database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.down.sql @@ -0,0 +1,2 @@ +DROP INDEX sync_group_listings_sync_group_id_platform_shop_id_idx; +DROP INDEX sync_group_listing_drafts_account_id_platform_shop_id_idx; diff --git a/database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.up.sql b/database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.up.sql new file mode 100644 index 0000000..3cc2ac7 --- /dev/null +++ b/database_migrations/000014_disallow_multiple_listings_per_store_per_sync_group.up.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX ON sync_group_listings (sync_group_id, platform, shop_id) NULLS DISTINCT; +CREATE UNIQUE INDEX ON sync_group_listing_drafts (account_id, platform, shop_id) NULLS DISTINCT; diff --git a/go.mod b/go.mod index fcc4f5f..650a829 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require github.com/jackc/pgx/v5 v5.7.6 require ( github.com/angelbeltran/templater v0.1.0 github.com/coreos/go-oidc/v3 v3.8.0 + github.com/gin-gonic/gin v1.9.1 github.com/google/uuid v1.5.0 github.com/lmittmann/tint v1.1.2 github.com/oapi-codegen/runtime v1.1.2 @@ -22,35 +23,55 @@ require ( github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/bytedance/sonic v1.10.0-rc3 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/getkin/kin-openapi v0.133.0 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect + golang.org/x/arch v0.4.0 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 9f8fecb..c3a5c21 100644 --- a/go.sum +++ b/go.sum @@ -7,17 +7,21 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs= github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/angelbeltran/concurry v0.0.0-20260109213122-ef9f8bd8865f h1:9KBIsU6vHBWD/BGvxaFl45SNOE5aWHibZ3F5MdfFPsQ= -github.com/angelbeltran/concurry v0.0.0-20260109213122-ef9f8bd8865f/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= -github.com/angelbeltran/concurry v0.0.0-20260109213220-20157174e154 h1:HFkK1uyOQ5nZgrQRyvebyLPMJGkouRAS26ljn8EsU0g= -github.com/angelbeltran/concurry v0.0.0-20260109213220-20157174e154/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= -github.com/angelbeltran/concurry v0.0.0-20260109224857-024377724e7e h1:bsEp3zk309jiuKMwi5pS2rjolwvVLCfpSwUSNM/MZ04= -github.com/angelbeltran/concurry v0.0.0-20260109224857-024377724e7e/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c h1:BIa061RHaIVP9TjpIQf0PldVpQmeln/2U1Cf2w8ALNI= github.com/angelbeltran/concurry v0.0.0-20260109231127-8e2678d8928c/go.mod h1:Aii6F8lZCJb8Ns0d1FH35bDxNoyiuvwXd5BYJroJnks= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0= +github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -33,17 +37,33 @@ github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5ql github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +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.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.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= +github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -62,6 +82,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -79,7 +100,13 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 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= @@ -87,12 +114,21 @@ 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/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -118,6 +154,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -139,13 +177,22 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= @@ -155,6 +202,9 @@ github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4Z github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= +golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -164,8 +214,6 @@ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -203,6 +251,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -218,8 +268,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -255,3 +303,5 @@ gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/domains/accounts/sync_groups.go b/internal/domains/accounts/sync_groups.go index 33643e8..6867702 100644 --- a/internal/domains/accounts/sync_groups.go +++ b/internal/domains/accounts/sync_groups.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "ruben/inventory2/internal/consts" + "slices" "github.com/jackc/pgx/v5" ) @@ -18,7 +19,8 @@ type ( SyncGroupListing struct { SyncGroupIDs AccountShopIDs - ListingID string + ListingID string + OrderIndex int } SyncGroupIDs struct { @@ -28,7 +30,8 @@ type ( SyncGroupListingDraft struct { AccountShopIDs - ListingID string + ListingID string + OrderIndex int } ) @@ -96,9 +99,10 @@ func (db *Store) GetSyncGroupListingDraft(ctx context.Context, acctID int64, ord } r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct { - Platform *Platform - Shop_id *string - Listing_id *string + Platform *Platform + Shop_id *string + Listing_id *string + Order_index *int }]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -115,7 +119,8 @@ func (db *Store) GetSyncGroupListingDraft(ctx context.Context, acctID int64, ord Platform: deref(r.Platform), ShopID: deref(r.Shop_id), }, - ListingID: deref(r.Listing_id), + ListingID: deref(r.Listing_id), + OrderIndex: orderIndex, }, nil } @@ -182,17 +187,7 @@ func (db *Store) DeleteSyncGroupListingDraft(ctx context.Context, acctID int64, rows, err := db.db.Query( ctx, ` - WITH updated_drafts AS ( - UPDATE - sync_group_listing_drafts - SET - order_index = (order_index - 1) - WHERE - account_id = @account_id - AND order_index > @order_index - RETURNING - order_index - ), deleted_draft AS ( + WITH deleted_draft AS ( DELETE FROM sync_group_listing_drafts WHERE @@ -202,17 +197,17 @@ func (db *Store) DeleteSyncGroupListingDraft(ctx context.Context, acctID int64, true AS found ) SELECT - COALESCE(dd.found, false) AS found, - (COALESCE(MAX(ud.order_index), -1) + 1) AS num_rows + COUNT(*) as num_rows, + COALESCE(dd.found, FALSE) as found FROM - deleted_draft dd + sync_group_listing_drafts ld LEFT JOIN - updated_drafts ud - ON true + deleted_draft dd + ON TRUE + WHERE + account_id = @account_id GROUP BY - ud.order_index, dd.found - LIMIT - 1 + ld.account_id, dd.found `, pgx.NamedArgs{ "account_id": acctID, @@ -256,6 +251,8 @@ func (db *Store) GetSyncGroupListingDrafts(ctx context.Context, acctID int64) ([ sync_group_listing_drafts WHERE account_id = @account_id + ORDER BY + order_index ASC `, pgx.NamedArgs{ "account_id": acctID, @@ -278,8 +275,8 @@ func (db *Store) GetSyncGroupListingDrafts(ctx context.Context, acctID int64) ([ } listings := make([]SyncGroupListingDraft, len(rs)) - for _, r := range rs { - listings[r.Order_index] = SyncGroupListingDraft{ + for i, r := range rs { + listings[i] = SyncGroupListingDraft{ AccountShopIDs: AccountShopIDs{ AccountIDs: AccountIDs{ AccountID: acctID, @@ -287,10 +284,15 @@ func (db *Store) GetSyncGroupListingDrafts(ctx context.Context, acctID int64) ([ Platform: deref(r.Platform), ShopID: deref(r.Shop_id), }, - ListingID: deref(r.Listing_id), + ListingID: deref(r.Listing_id), + OrderIndex: r.Order_index, } } + slices.SortFunc(listings, func(a, b SyncGroupListingDraft) int { + return a.OrderIndex - b.OrderIndex + }) + return listings, nil } @@ -397,8 +399,8 @@ func (db *Store) SaveNewSyncGroup(ctx context.Context, acctID int64) (SyncGroup, } listings := make([]SyncGroupListing, len(rs)) - for _, r := range rs { - listings[r.Order_index] = SyncGroupListing{ + for i, r := range rs { + listings[i] = SyncGroupListing{ SyncGroupIDs: SyncGroupIDs{ AccountIDs: AccountIDs{ AccountID: acctID, @@ -412,10 +414,15 @@ func (db *Store) SaveNewSyncGroup(ctx context.Context, acctID int64) (SyncGroup, Platform: r.Platform, ShopID: r.Shop_id, }, - ListingID: r.Listing_id, + ListingID: r.Listing_id, + OrderIndex: r.Order_index, } } + slices.SortFunc(listings, func(a, b SyncGroupListing) int { + return a.OrderIndex - b.OrderIndex + }) + return SyncGroup{ SyncGroupIDs: SyncGroupIDs{ AccountIDs: AccountIDs{ diff --git a/internal/domains/authentication/auth.go b/internal/domains/authentication/auth.go index 098e869..7b69009 100644 --- a/internal/domains/authentication/auth.go +++ b/internal/domains/authentication/auth.go @@ -31,7 +31,7 @@ const ( AUTH0_CLIENT_SECRET = "83U-iWdVaNnwk9XDzteo_2VMyOq_l1siKYqg1_2E7jCzgL8MnkaxlysPMcPMGlxA" // The Callback URL of our application. - AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/auth/login/callback" + AUTH0_CALLBACK_URL = "https://inventory-plus-plus.com/api/auth/login/callback" ) type ( diff --git a/internal/server/api/accounts/router.go b/internal/server/api/accounts/router.go index f79bd88..e6881ac 100644 --- a/internal/server/api/accounts/router.go +++ b/internal/server/api/accounts/router.go @@ -9,44 +9,41 @@ import ( "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server/middleware" "ruben/inventory2/internal/server/response" - "ruben/inventory2/internal/server/router" "strconv" + + "github.com/gin-gonic/gin" ) type accountSubrouter struct { - log *logging.Logger - *router.SubMux + log *logging.Logger accts *accounts.Store } -func NewAccountSubrouter( +func Routes( + r *gin.RouterGroup, logger *logging.Logger, accts *accounts.Store, authMiddleware *middleware.Auth, -) *accountSubrouter { - mux := router.NewSubMux(logger) - +) { as := &accountSubrouter{ - log: logger, - SubMux: mux, - accts: accts, + log: logger, + accts: accts, } - withAuth := func(fn response.HandlerFunc) response.HandlerFunc { - return authMiddleware.AuthenticateAndAddIdentity(fn) + withAuth := func(fn response.HandlerFunc) gin.HandlerFunc { + return response.Handler(authMiddleware.AuthenticateAndAddIdentity(fn)) } - mux.Handle("POST /{acctID}/inventory/sync-groups/draft/listings", withAuth(as.createSyncGroupListingDraft)) - mux.Handle("PUT /{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop", withAuth(as.setShopInSyncGroupListingDraft)) - mux.Handle("PUT /{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing", withAuth(as.setListingInSyncGroupListingDraft)) - mux.Handle("DELETE /{acctID}/inventory/sync-groups/draft/listings/{orderIndex}", withAuth(as.deleteSyncGroupListingDraft)) - mux.Handle("POST /{acctID}/inventory/sync-groups", withAuth(as.saveNewSyncGroup)) - - return as + r.POST("/:acctID/inventory/sync-groups/draft/listings", withAuth(as.createSyncGroupListingDraft)) + r.PUT("/:acctID/inventory/sync-groups/draft/listings/:orderIndex/shop", withAuth(as.setShopInSyncGroupListingDraft)) + r.PUT("/:acctID/inventory/sync-groups/draft/listings/:orderIndex/listing", withAuth(as.setListingInSyncGroupListingDraft)) + r.DELETE("/:acctID/inventory/sync-groups/draft/listings/:orderIndex", withAuth(as.deleteSyncGroupListingDraft)) + r.POST("/:acctID/inventory/sync-groups", withAuth(as.saveNewSyncGroup)) } -// POST /accounts -func (s *accountSubrouter) createAccount(r *http.Request) (response.Response, error) { +// POST /api/accounts +func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() email := r.FormValue("email") if email == "" { @@ -64,11 +61,12 @@ func (s *accountSubrouter) createAccount(r *http.Request) (response.Response, er return nil, response.Errorf("failed to create account: %w", err) } - return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.AccountID)), nil + return response.SeeOther(fmt.Sprintf("/api/accounts/%d", acct.AccountID)), nil } -// POST /accounts/{acctID}/inventory/sync-groups/draft/listings -func (s *accountSubrouter) createSyncGroupListingDraft(r *http.Request) (response.Response, error) { +// POST /api/accounts/{acctID}/inventory/sync-groups/draft/listings +func (s *accountSubrouter) createSyncGroupListingDraft(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() acctID := middleware.GetIdentity(ctx).Account.AccountID @@ -79,18 +77,19 @@ func (s *accountSubrouter) createSyncGroupListingDraft(r *http.Request) (respons return response.Redirect( http.StatusSeeOther, - fmt.Sprintf("/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex), + fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex), ), nil } -// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop +// PUT /api/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/shop // @platform string // @shopID string -func (s *accountSubrouter) setShopInSyncGroupListingDraft(r *http.Request) (response.Response, error) { +func (s *accountSubrouter) setShopInSyncGroupListingDraft(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() acctID := middleware.GetIdentity(ctx).Account.AccountID - orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(r) + orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c) if err != nil { return nil, err } @@ -114,16 +113,17 @@ func (s *accountSubrouter) setShopInSyncGroupListingDraft(r *http.Request) (resp return response.Redirect( http.StatusSeeOther, - fmt.Sprintf("/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex), + fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex), ), nil } -// PUT /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing -func (s *accountSubrouter) setListingInSyncGroupListingDraft(r *http.Request) (response.Response, error) { +// PUT /api/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}/listing +func (s *accountSubrouter) setListingInSyncGroupListingDraft(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() acctID := middleware.GetIdentity(ctx).Account.AccountID - orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(r) + orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c) if err != nil { return nil, err } @@ -140,16 +140,17 @@ func (s *accountSubrouter) setListingInSyncGroupListingDraft(r *http.Request) (r return response.Redirect( http.StatusSeeOther, - fmt.Sprintf("/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex), + fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/draft/listings/%d", acctID, orderIndex), ), nil } -// DELETE /accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex} -func (s *accountSubrouter) deleteSyncGroupListingDraft(r *http.Request) (response.Response, error) { +// DELETE /api/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex} +func (s *accountSubrouter) deleteSyncGroupListingDraft(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() acctID := middleware.GetIdentity(ctx).Account.AccountID - orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(r) + orderIndex, err := getOrderIndexForSyncGroupListingDraftFromPath(c) if err != nil { return nil, err } @@ -161,8 +162,9 @@ func (s *accountSubrouter) deleteSyncGroupListingDraft(r *http.Request) (respons return response.Status(200), nil } -// POST /accounts/{acctID}/inventory/sync-groups -func (s *accountSubrouter) saveNewSyncGroup(r *http.Request) (response.Response, error) { +// POST /api/accounts/{acctID}/inventory/sync-groups +func (s *accountSubrouter) saveNewSyncGroup(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() acctID := middleware.GetIdentity(ctx).Account.AccountID @@ -174,12 +176,12 @@ func (s *accountSubrouter) saveNewSyncGroup(r *http.Request) (response.Response, return response.Redirect( http.StatusSeeOther, // TODO: template not implemented - fmt.Sprintf("/accounts/%d/inventory/sync-groups/%d", acctID, grp.SyncGroupID), + fmt.Sprintf("/ui/accounts/%d/inventory/sync-groups/%d", acctID, grp.SyncGroupID), ), nil } -func getOrderIndexForSyncGroupListingDraftFromPath(r *http.Request) (int, error) { - orderIndexStr := r.PathValue("orderIndex") +func getOrderIndexForSyncGroupListingDraftFromPath(c *gin.Context) (int, error) { + orderIndexStr := c.Param("orderIndex") if orderIndexStr == "" { return 0, response.NotFound(). Msg("no orderIndex found") diff --git a/internal/server/api/auth/router.go b/internal/server/api/auth/router.go index d074e86..ee943fc 100644 --- a/internal/server/api/auth/router.go +++ b/internal/server/api/auth/router.go @@ -3,53 +3,36 @@ package auth import ( "context" "fmt" - "net/http" "ruben/inventory2/internal/domains/authentication" "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server/cookies" "ruben/inventory2/internal/server/response" - "ruben/inventory2/internal/server/router" + + "github.com/gin-gonic/gin" ) type loginSubrouter struct { log *logging.Logger auth *authentication.Authenticator - router.Subrouter } -func NewLoginSubrouter(logger *logging.Logger, auth *authentication.Authenticator) *loginSubrouter { - mux := router.NewSubMux(logger) - +func Routes( + r *gin.RouterGroup, + logger *logging.Logger, + auth *authentication.Authenticator, +) { ls := &loginSubrouter{ - log: logger, - auth: auth, - Subrouter: mux, + log: logger, + auth: auth, } - mux.Handle("GET /login", ls.loginPage) - mux.Handle("GET /login/callback", ls.loginCallback) - mux.Handle("GET /logout", ls.logoutPage) - - return ls + r.GET("/login", response.Handler(ls.loginPage)) + r.GET("/login/callback", response.Handler(ls.loginCallback)) + r.GET("/logout", response.Handler(ls.logoutPage)) } -func (s *loginSubrouter) newLoginSubrouter(logger *logging.Logger) router.Subrouter { - mux := router.NewSubMux(logger) - - ls := &loginSubrouter{ - log: s.log, - auth: s.auth, - Subrouter: mux, - } - - mux.Handle("GET /login", ls.loginPage) - mux.Handle("GET /login/callback", ls.loginCallback) - mux.Handle("GET /logout", ls.logoutPage) - - return ls -} - -func (s *loginSubrouter) loginPage(r *http.Request) (response.Response, error) { +func (s *loginSubrouter) loginPage(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() u, err := NewLoginURL(ctx, s.auth, "/") @@ -71,7 +54,8 @@ func NewLoginURL(ctx context.Context, auth *authentication.Authenticator, target return auth.AuthCodeURL(base64EncodedState), nil } -func (s *loginSubrouter) loginCallback(r *http.Request) (response.Response, error) { +func (s *loginSubrouter) loginCallback(c *gin.Context) (response.Response, error) { + r := c.Request ctx := r.Context() q := r.URL.Query() @@ -90,7 +74,9 @@ func (s *loginSubrouter) loginCallback(r *http.Request) (response.Response, erro Cookie(cookies.AccessToken(accessToken, expiration)), nil } -func (s *loginSubrouter) logoutPage(r *http.Request) (response.Response, error) { +func (s *loginSubrouter) logoutPage(c *gin.Context) (response.Response, error) { + r := c.Request + host := r.Header.Get("X-Forwarded-Host") if host == "" { host = r.Host diff --git a/internal/server/api/templates/router.go b/internal/server/api/templates/router.go index 503a0cf..8adffe8 100644 --- a/internal/server/api/templates/router.go +++ b/internal/server/api/templates/router.go @@ -17,55 +17,74 @@ import ( "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server/middleware" "ruben/inventory2/internal/server/response" - "ruben/inventory2/internal/server/router" "github.com/angelbeltran/templater" + "github.com/gin-gonic/gin" ) -type webpageRouter struct { - log *logging.Logger - contentDir string - templater *templater.Templater - rawEvents *raw_events.Store - accts *accounts.Store - etsy *etsy_platform.Platform - router.Subrouter -} +type ( + webpageRouter struct { + log *logging.Logger + contentDir string + templater *templater.Templater + rawEvents *raw_events.Store + accts *accounts.Store + etsy *etsy_platform.Platform + authMiddleware *middleware.Auth + } -func NewWebpageRouter( + // ErrTemplateNotFound is returned if the reason the template failed to compile + // is due to the template not being found. + ErrTemplateNotFound struct { + err error + } +) + +func SetupRoutes( logger *logging.Logger, + r gin.IRouter, contentDir string, tmpl *templater.Templater, rawEvents *raw_events.Store, accts *accounts.Store, etsy *etsy_platform.Platform, authMiddleware *middleware.Auth, -) *webpageRouter { - mux := router.NewSubMux(logger) - - wr := &webpageRouter{ - log: logger, - contentDir: contentDir, - templater: tmpl, - rawEvents: rawEvents, - accts: accts, - etsy: etsy, - Subrouter: mux, +) { + s := &webpageRouter{ + log: logger, + contentDir: contentDir, + templater: tmpl, + rawEvents: rawEvents, + accts: accts, + etsy: etsy, + authMiddleware: authMiddleware, } - // non-authenticated - mux.Handle("GET /{$}", authMiddleware.AddIdentity(wr.serveTemplates)) + fn2 := s.authMiddleware.AuthenticateAndAddIdentity(s.serveTemplates) - // authenticated - mux.Handle("GET /", authMiddleware.AuthenticateAndAddIdentity(wr.serveTemplates)) + r.GET("/*rest", response.Handler(func(c *gin.Context) (response.Response, error) { + c.Request.URL.Path = c.Request.URL.Path[3:] + defer func() { + c.Request.URL.Path = "/ui" + c.Request.URL.Path + }() - return wr + p := c.Request.URL.Path + if p == "/" || p == "" { + c, err := s.authMiddleware.AddIdentityToRequest(c) + if err != nil { + return nil, err + } + return s.serveTemplates(c) + } + return fn2(c) + })) } // GET / // compiles the page template or component template matching the url // func (s *Server) serveTemplates(r *http.Request) (response.Response, error) { -func (s *webpageRouter) serveTemplates(r *http.Request) (response.Response, error) { +func (s *webpageRouter) serveTemplates(c *gin.Context) (response.Response, error) { + r := c.Request name, args := s.getTemplateNameAndArgs(r, s.contentDir+"/templates/component_bodies") b, err := s.templater.ExecuteComponentBody(name, args...) @@ -190,7 +209,9 @@ func getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(file func (s *webpageRouter) handleTemplateError(err error, templateArgs ...any) (response.Response, error) { if isFileNotFoundError(err) { return nil, response.NotFound(). - Wrap(err). + Wrap(ErrTemplateNotFound{ + err: err, + }). Msg("resource not found") } @@ -280,3 +301,14 @@ func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error return nil } + +func (e ErrTemplateNotFound) Error() string { + if e.err != nil { + return fmt.Sprintf("template not found: %v", e.err) + } + return fmt.Sprintf("template not found") +} + +func (e ErrTemplateNotFound) Unwrap() error { + return e.err +} diff --git a/internal/server/api/webhooks/etsy/webhooks.go b/internal/server/api/webhooks/etsy/webhooks.go index 95fc228..ea2406a 100644 --- a/internal/server/api/webhooks/etsy/webhooks.go +++ b/internal/server/api/webhooks/etsy/webhooks.go @@ -3,58 +3,60 @@ package etsy import ( "encoding/json" "fmt" - "net/http" "strconv" "time" "ruben/inventory2/internal/domains/platforms/etsy" "ruben/inventory2/internal/domains/raw_events" "ruben/inventory2/internal/logging" + "ruben/inventory2/internal/server/response" + + "github.com/gin-gonic/gin" ) type ( - Webhooks struct { - log *logging.Logger - cfg Config - db *raw_events.Store - etsy *etsy.Platform - } - Config struct { OAuthRedirectURIWithAcctIDParam string } ) -func NewWebhookHandler( +func Webhooks( + r *gin.RouterGroup, logger *logging.Logger, db *raw_events.Store, platform *etsy.Platform, cfg Config, -) http.Handler { - h := Webhooks{ +) { + h := webhooks{ log: logger, cfg: cfg, db: db, etsy: platform, } - mux := http.NewServeMux() + r.POST("/test", response.Handler(h.test)) - mux.HandleFunc("POST /test", h.test) + r.GET(h.cfg.OAuthRedirectURIWithAcctIDParam, response.Handler(h.redirectURI)) - mux.HandleFunc("GET "+h.cfg.OAuthRedirectURIWithAcctIDParam, h.redirectURI) - - mux.HandleFunc("GET /{acctID}/new-account-link", h.newAccountLink) - - return mux + r.GET("/{acctID}/new-account-link", response.Handler(h.newAccountLink)) } +type ( + webhooks struct { + log *logging.Logger + cfg Config + db *raw_events.Store + etsy *etsy.Platform + } +) + // POST /test -func (h Webhooks) test(w http.ResponseWriter, r *http.Request) { +func (h webhooks) test(c *gin.Context) (response.Response, error) { + r := c.Request + var body json.RawMessage if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "Failed to decode body as json: "+err.Error(), 500) - return + return nil, response.Errorf("failed to decode body as json: %w", err) } ts := time.Now().UTC() @@ -75,21 +77,21 @@ func (h Webhooks) test(w http.ResponseWriter, r *http.Request) { Payload: body, }) if err != nil { - http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500) - return + return nil, response.Errorf("error occurred saving the body as the event payload: %w", err) } - w.WriteHeader(201) + return response.Status(201), nil } // GET h.cfg.OAuthRedirectURIWithAcctIDParam -func (h Webhooks) redirectURI(w http.ResponseWriter, r *http.Request) { +func (h webhooks) redirectURI(c *gin.Context) (response.Response, error) { + r := c.Request + // get account id for the request - acctID, err := strconv.ParseInt(r.PathValue("acctID"), 10, 64) + acctID, err := strconv.ParseInt(c.Param("acctID"), 10, 64) if err != nil || acctID <= 0 { - w.WriteHeader(http.StatusNotFound) - return + return nil, response.NotFound() } ctx := r.Context() @@ -113,41 +115,38 @@ func (h Webhooks) redirectURI(w http.ResponseWriter, r *http.Request) { h.etsy.InvalidateState(ctx, state) - return + return response.Status(200), nil } // validate the state to prevent CSRF attacks ok, err := h.etsy.HandleNewAuthCode(ctx, acctID, state, q.Get("code")) if err != nil { - w.WriteHeader(http.StatusForbidden) h.log.Error("failed to handle new auth code", "error", err) - return + return nil, response.Forbidden() } if !ok { - w.WriteHeader(http.StatusForbidden) - return + return nil, response.Forbidden() } // redirect to the user's account page - http.Redirect(w, r, fmt.Sprintf("/accounts/%d", acctID), http.StatusSeeOther) + return response.SeeOther(fmt.Sprintf("/accounts/%d", acctID)), nil } // GET /{acctID}/new-account-link -func (h Webhooks) newAccountLink(w http.ResponseWriter, r *http.Request) { - acctIDStr := r.PathValue("acctID") +func (h webhooks) newAccountLink(c *gin.Context) (response.Response, error) { + r := c.Request + acctIDStr := c.Param("acctID") acctID, err := strconv.ParseInt(acctIDStr, 10, 64) if err != nil { - http.Error(w, fmt.Sprintf("account %s not found", acctIDStr), http.StatusNotFound) - return + return nil, response.NotFound().Msgf("account %s not found", acctIDStr) } u, err := h.etsy.GenerateConnectionURLForNewAccount(r.Context(), acctID) if err != nil { - http.Error(w, fmt.Sprintf("failed to generate url for account %d: %v", acctID, err), http.StatusInternalServerError) - return + return nil, fmt.Errorf("failed to generate url for account %d: %w", acctID, err) } - http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect) + return response.TemporaryRedirect(u.String()), nil } diff --git a/internal/server/api/webhooks/tiktok/webhooks.go b/internal/server/api/webhooks/tiktok/webhooks.go index 69e0fe8..6b98982 100644 --- a/internal/server/api/webhooks/tiktok/webhooks.go +++ b/internal/server/api/webhooks/tiktok/webhooks.go @@ -3,21 +3,26 @@ package tiktok import ( "encoding/json" "fmt" - "net/http" "time" "ruben/inventory2/internal/domains/raw_events" "ruben/inventory2/internal/logging" + "ruben/inventory2/internal/server/response" + + "github.com/gin-gonic/gin" ) -func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handler { - mux := http.NewServeMux() +func Webhooks( + r *gin.RouterGroup, + logger *logging.Logger, + db *raw_events.Store, +) { + r.POST("/test", response.Handler(func(c *gin.Context) (response.Response, error) { + r := c.Request - mux.HandleFunc("POST /test", func(w http.ResponseWriter, r *http.Request) { var body json.RawMessage if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "Failed to decode body as json: "+err.Error(), 500) - return + return nil, fmt.Errorf("failed to decode body as json: %w", err) } ts := time.Now().UTC() @@ -30,12 +35,9 @@ func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handle Payload: body, }) if err != nil { - http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500) - return + return nil, fmt.Errorf("error occurred saving the body as the event payload: %w", err) } - w.WriteHeader(201) - }) - - return mux + return response.Status(201), nil + })) } diff --git a/internal/server/api/webhooks/webhooks.go b/internal/server/api/webhooks/webhooks.go index 3eed72e..6d7ffae 100644 --- a/internal/server/api/webhooks/webhooks.go +++ b/internal/server/api/webhooks/webhooks.go @@ -1,47 +1,42 @@ package webhooks import ( - "net/http" - etsy_platform "ruben/inventory2/internal/domains/platforms/etsy" "ruben/inventory2/internal/domains/raw_events" "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server/api/webhooks/etsy" "ruben/inventory2/internal/server/api/webhooks/tiktok" "ruben/inventory2/internal/server/api/webhooks/wix" + + "github.com/gin-gonic/gin" ) type Config struct { Etsy etsy.Config } -// TODO: just move over to the 'site' package, and then consider renaming the site package to something else? -func New(logger *logging.Logger, eventsDB *raw_events.Store, etsyPlatform *etsy_platform.Platform, cfg Config) http.Handler { - wh := http.NewServeMux() - - wh.Handle( - "/etsy/", - http.StripPrefix("/etsy", etsy.NewWebhookHandler( - logger.WithGroup("etsy"), - eventsDB, - etsyPlatform, - cfg.Etsy, - )), +func Webhooks( + r *gin.RouterGroup, + logger *logging.Logger, + eventsDB *raw_events.Store, + etsyPlatform *etsy_platform.Platform, + cfg Config, +) { + etsy.Webhooks( + r.Group("/etsy"), + logger.WithGroup("etsy"), + eventsDB, + etsyPlatform, + cfg.Etsy, ) - wh.Handle( - "/tiktok/", - http.StripPrefix("/tiktok", tiktok.NewWebhookHandler( - logger.WithGroup("tiktok"), - eventsDB, - )), + tiktok.Webhooks( + r.Group("/tiktok"), + logger.WithGroup("tiktok"), + eventsDB, ) - wh.Handle( - "/wix/", - http.StripPrefix("/wix", wix.NewWebhookHandler( - logger.WithGroup("wix"), - eventsDB, - )), + wix.Webhooks( + r.Group("/wix"), + logger.WithGroup("wix"), + eventsDB, ) - - return wh } diff --git a/internal/server/api/webhooks/wix/webhooks.go b/internal/server/api/webhooks/wix/webhooks.go index 321d7a5..f432b6f 100644 --- a/internal/server/api/webhooks/wix/webhooks.go +++ b/internal/server/api/webhooks/wix/webhooks.go @@ -3,21 +3,26 @@ package wix import ( "encoding/json" "fmt" - "net/http" "time" "ruben/inventory2/internal/domains/raw_events" "ruben/inventory2/internal/logging" + "ruben/inventory2/internal/server/response" + + "github.com/gin-gonic/gin" ) -func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handler { - mux := http.NewServeMux() +func Webhooks( + r *gin.RouterGroup, + logger *logging.Logger, + db *raw_events.Store, +) { + r.POST("/test", response.Handler(func(c *gin.Context) (response.Response, error) { + r := c.Request - mux.HandleFunc("POST /test", func(w http.ResponseWriter, r *http.Request) { var body json.RawMessage if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - http.Error(w, "Failed to decode body as json: "+err.Error(), 500) - return + return nil, fmt.Errorf("failed to decode body as json: %w", err) } ts := time.Now().UTC() @@ -30,12 +35,9 @@ func NewWebhookHandler(logger *logging.Logger, db *raw_events.Store) http.Handle Payload: body, }) if err != nil { - http.Error(w, "Error occurred saving the body as the event payload: "+err.Error(), 500) - return + return nil, fmt.Errorf("error occurred saving the body as the event payload: %w", err) } - w.WriteHeader(201) - }) - - return mux + return response.Status(201), nil + })) } diff --git a/internal/server/middleware/auth.go b/internal/server/middleware/auth.go index 8e6a7ef..2ec683b 100644 --- a/internal/server/middleware/auth.go +++ b/internal/server/middleware/auth.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "net/http" "time" "ruben/inventory2/internal/consts" @@ -15,6 +14,8 @@ import ( "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server/cookies" "ruben/inventory2/internal/server/response" + + "github.com/gin-gonic/gin" ) type ( @@ -52,20 +53,22 @@ func NewAuth( } func (a *Auth) AddIdentity(fn response.HandlerFunc) response.HandlerFunc { - return func(r *http.Request) (response.Response, error) { - r, err := a.AddIdentityToRequest(r) + return func(c *gin.Context) (response.Response, error) { + c, err := a.AddIdentityToRequest(c) if err != nil { return nil, err } - return fn(r) + return fn(c) } } -func (a *Auth) AddIdentityToRequest(r *http.Request) (*http.Request, error) { +func (a *Auth) AddIdentityToRequest(c *gin.Context) (*gin.Context, error) { + r := c.Request + ck, err := r.Cookie("access_token") if err != nil { - return r, nil + return c, nil } ctx := r.Context() @@ -75,27 +78,29 @@ func (a *Auth) AddIdentityToRequest(r *http.Request) (*http.Request, error) { claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken) if err != nil { if errors.Is(err, consts.ErrNotFound) { - return r, nil + return c, nil } - return r, response.Errorf("failed to load authentication details: %w", err) + return c, response.Errorf("failed to load authentication details: %w", err) } if expiration.Before(time.Now()) { - return r, nil + return c, nil } user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken) if err != nil { - return r, response.Errorf("failed to load user and account defails: %w", err) + return c, response.Errorf("failed to load user and account defails: %w", err) } - return r.WithContext(SetIdentity(ctx, Identity{ + c.Request = r.WithContext(SetIdentity(ctx, Identity{ AccessToken: accessToken, Claims: claims, User: user, Account: acct, - })), nil + })) + + return c, nil } // TODO: after getting auth, consider making http handler functions take 'claims', etc, as function arguments @@ -103,73 +108,86 @@ func (a *Auth) AddIdentityToRequest(r *http.Request) (*http.Request, error) { // auth middleware to verify access_token cookie and set custom claims in the request context func (a *Auth) AuthenticateAndAddIdentity(f response.HandlerFunc, assertions ...AuthorizationAssertions) response.HandlerFunc { - return func(r *http.Request) (response.Response, error) { - ck, err := r.Cookie("access_token") - if err != nil { - return response.TemporaryRedirect("/"). - JSON("no access_token cookie provided"), nil + return func(c *gin.Context) (response.Response, error) { + c, res, err := a.AuthenticateAndAddIdentityToRequest(c, assertions...) + if res != nil || err != nil { + return res, nil } - ctx := r.Context() - - accessToken := ck.Value - - claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - u, err := a.newLoginURL(ctx, a.auth, r.URL.String()) - if err != nil { - return nil, response.Errorf("failed to generate login url: %w", err) - } - - return response.TemporaryRedirect(u), nil - } - - return nil, response.Errorf("failed to authenticate: %w", err) - } - - now := time.Now() - - // refresh tokens, when the access token is "old enough" - - // id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in. - const idTokenLifetime = 48 * time.Hour - if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) { - accessToken, expiration, err = a.auth.RefreshAccessToken(ctx, accessToken) - if err != nil { - a.log.Warn("failed to refresh access token", "error", err) - return response.TemporaryRedirect("/"). - Body(io.NopCloser(bytes.NewBuffer([]byte(fmt.Sprintf("failed to refresh access token: %v", err))))). - Cookie(cookies.Expired("access_token")), nil - } - - // 'redirect' to same url, to set the new access_token cookie - return response.TemporaryRedirect(r.URL.String()). - Cookie(cookies.AccessToken(accessToken, expiration)), nil - } - - // add identity info to request context - - user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken) - if err != nil { - return nil, response.Errorf("failed to authorize: %w", err) - } - - for _, as := range assertions { - if res, err := as(r); res != nil || err != nil { - return res, err - } - } - - return f(r.WithContext(SetIdentity(ctx, Identity{ - AccessToken: accessToken, - Claims: claims, - User: user, - Account: acct, - }))) + return f(c) } } +func (a *Auth) AuthenticateAndAddIdentityToRequest(c *gin.Context, assertions ...AuthorizationAssertions) (*gin.Context, response.Response, error) { + r := c.Request + ck, err := r.Cookie("access_token") + if err != nil { + return c, response.TemporaryRedirect("/"). + JSON("no access_token cookie provided"), nil + } + + ctx := r.Context() + + accessToken := ck.Value + + claims, expiration, err := a.auth.GetAccessTokenClaimsAndExpiration(ctx, accessToken) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + u, err := a.newLoginURL(ctx, a.auth, r.URL.String()) + if err != nil { + return c, nil, response.Errorf("failed to generate login url: %w", err) + } + + return c, response.TemporaryRedirect(u), nil + } + + return c, nil, response.Errorf("failed to authenticate: %w", err) + } + + now := time.Now() + + // refresh tokens, when the access token is "old enough" + + // id token lifetime is 48 hours, allowing a person to use the app everyday comfortably, with wiggle room, without having to log in. + const idTokenLifetime = 48 * time.Hour + if refreshFloor := expiration.Add(-(idTokenLifetime / 4)); refreshFloor.Before(now) { + accessToken, expiration, err = a.auth.RefreshAccessToken(ctx, accessToken) + if err != nil { + a.log.Warn("failed to refresh access token", "error", err) + return c, response.TemporaryRedirect("/"). + Body(io.NopCloser(bytes.NewBuffer([]byte(fmt.Sprintf("failed to refresh access token: %v", err))))). + Cookie(cookies.Expired("access_token")), nil + } + + // 'redirect' to same url, to set the new access_token cookie + return c, response.TemporaryRedirect(r.URL.String()). + Cookie(cookies.AccessToken(accessToken, expiration)), nil + } + + // add identity info to request context + + user, acct, err := a.accts.GetUserAndAccountByAccessToken(ctx, accessToken) + if err != nil { + return c, nil, response.Errorf("failed to authorize: %w", err) + } + + for _, as := range assertions { + if res, err := as(c); res != nil || err != nil { + return c, res, err + } + } + + r = r.WithContext(SetIdentity(ctx, Identity{ + AccessToken: accessToken, + Claims: claims, + User: user, + Account: acct, + })) + c.Request = r + + return c, nil, nil +} + type identityKey struct{} // stores identity in request context diff --git a/internal/server/middleware/log.go b/internal/server/middleware/log.go index f479329..056acfc 100644 --- a/internal/server/middleware/log.go +++ b/internal/server/middleware/log.go @@ -7,15 +7,18 @@ import ( "ruben/inventory2/internal/logging" "ruben/inventory2/internal/server/response" + + "github.com/gin-gonic/gin" ) func LogRequests(ctx context.Context, logger *logging.Logger) response.Middleware { reqIDCh := newRequestIDProvider(ctx) return func(fn response.HandlerFunc) response.HandlerFunc { - return func(r *http.Request) (response.Response, error) { + return func(c *gin.Context) (response.Response, error) { start := time.Now() + r := c.Request args := []any{ "id", <-reqIDCh, "url", r.URL, @@ -24,7 +27,7 @@ func LogRequests(ctx context.Context, logger *logging.Logger) response.Middlewar logger.Debug("Request", args...) - res, err := fn(r) + res, err := fn(c) end := time.Now() var status int diff --git a/internal/server/response/handler.go b/internal/server/response/handler.go index ea0d0a6..5da12f9 100644 --- a/internal/server/response/handler.go +++ b/internal/server/response/handler.go @@ -1,22 +1,22 @@ package response import ( - "net/http" + "github.com/gin-gonic/gin" ) type ( - HandlerFunc = func(r *http.Request) (Response, error) + HandlerFunc = func(c *gin.Context) (Response, error) Middleware = func(HandlerFunc) HandlerFunc ) -func Handler(f HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - res, err := f(r) +func Handler(f HandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + res, err := f(c) if err != nil { - WriteError(w, err) + WriteError(c, err) } else { - Write(w, r, res) + Write(c, res) } } } diff --git a/internal/server/response/write.go b/internal/server/response/write.go index b03de81..b2490de 100644 --- a/internal/server/response/write.go +++ b/internal/server/response/write.go @@ -4,9 +4,13 @@ import ( "fmt" "io" "net/http" + + "github.com/gin-gonic/gin" ) -func Write(w http.ResponseWriter, r *http.Request, res Response) { +func Write(c *gin.Context, res Response) { + w := c.Writer + // w.Header() must be set before ResponseWriter.WriteHeader is called // or redirect is attempted hdrs := w.Header() @@ -19,7 +23,7 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) { } if code, to, ok := res.GetRedirect(); ok { - http.Redirect(w, r, to, code.Int()) + http.Redirect(w, c.Request, to, code.Int()) return } @@ -47,8 +51,8 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) { } -func WriteError(w http.ResponseWriter, err error) { - http.Error(w, err.Error(), GetStatusFromError(err)) +func WriteError(c *gin.Context, err error) { + c.String(GetStatusFromError(err), err.Error()) } func GetStatusFromError(err error) int { diff --git a/internal/server/router/mux.go b/internal/server/router/mux.go deleted file mode 100644 index dc2007e..0000000 --- a/internal/server/router/mux.go +++ /dev/null @@ -1,659 +0,0 @@ -package router - -import ( - "fmt" - "net/http" - "path" - "slices" - "strings" - - "ruben/inventory2/internal/consts" - "ruben/inventory2/internal/logging" - "ruben/inventory2/internal/server/response" -) - -type ( - Mux struct { - Mux *http.ServeMux - middleware []response.Middleware - log *logging.Logger - } -) - -var ( - ErrHandlerNotFound = fmt.Errorf("%w: handler not found", consts.ErrNotFound) -) - -func NewMux(log *logging.Logger, ms ...response.Middleware) *Mux { - return &Mux{ - Mux: http.NewServeMux(), - middleware: ms, - log: log, - } -} - -func (m *Mux) AddMiddleware(ms ...response.Middleware) *Mux { - m.middleware = append(m.middleware, ms...) - return m -} - -func (m *Mux) Handle(pattern string, fn response.HandlerFunc) { - log := m.log.With( - "method", "Handle", - "patter", pattern, - "fn", fn, - ) - defer log.DebugCallf("called")() - m.Mux.Handle(pattern, response.Handler(m.applyMiddleware(fn))) -} - -func (m *Mux) applyMiddleware(fn response.HandlerFunc) response.HandlerFunc { - return applyMiddleware(fn, m.middleware...) -} - -func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { - m.Mux.ServeHTTP(w, r) -} - -// TODO: support for methods? -// TODO: strip the prefix from the call to the subrouter handler call -// Route does not accept methods or ... wildcards -// func (m *Mux) Route(basePathPattern string, sr Subrouter) { -func (m *Mux) Route(pattern string, sr Subrouter) { - log := m.log.With("method", "Route", "pattern", pattern, "sr", sr) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", nil - }) - - // --- - method, segments, _ := getHTTPMethodAndPathSegments(m.log, pattern) - - if pattern == "" || (len(segments) == 0 && pattern[len(pattern)-1] != '/') { - panic("invalid subpath: " + pattern) - } - - numPatternSegments := len(segments) - - handler := response.Handler(m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr))) - - cleanPattern := "/" - if joinedSegments := strings.Join(segments, "/"); joinedSegments != "" { - cleanPattern = "/" + joinedSegments + "/" - } - if method != "" { - cleanPattern = method + " " + cleanPattern - } - log.Debugf("cleanPattern: %s", cleanPattern) - // --- - - /* - trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/") - if trimmedSubpathPattern == "" && basePathPattern != "/" { - panic("invalid subpath: " + basePathPattern) - } - - var trimmedPatternSegments []string - if trimmedSubpathPattern != "" { - trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/") - } - numPatternSegments := len(trimmedPatternSegments) - - handler := response.Handler(m.applyMiddleware(buildSubrouterHandlerFunc(numPatternSegments, sr))) - - cleanPattern := "/" - if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" { - cleanPattern = "/" + joinedSegments + "/" - } - fmt.Println("Mux.Route: cleanPattern:", cleanPattern) - */ - - if method != "" { - m.Mux.Handle(method+" "+cleanPattern, handler) - } else { - m.Mux.Handle("GET "+cleanPattern, handler) - m.Mux.Handle("POST "+cleanPattern, handler) - m.Mux.Handle("PUT "+cleanPattern, handler) - m.Mux.Handle("PATCH "+cleanPattern, handler) - m.Mux.Handle("DELETE "+cleanPattern, handler) - } - -} - -type ( - Subrouter interface { - Handler(r *http.Request) (fn response.HandlerFunc, pathParams map[string]string, found bool) - } - - SubMux struct { - tree *muxTree - middleware []response.Middleware - log *logging.Logger - } -) - -func NewSubMux(log *logging.Logger, ms ...response.Middleware) *SubMux { - return &SubMux{ - tree: newMuxTree(log.WithGroup("muxTree")), - middleware: ms, - log: log, - } -} - -// TODO: this is capturing all subroutes! -func (m *SubMux) Handle(pattern string, fn response.HandlerFunc) { - defer m.log.With("pattern", pattern, "fn", fn).DebugCallf("Handle")() - m.tree.set(pattern, m.applyMiddleware(fn)) -} - -// TODO: need http method support -// Route does not accept methods or ... wildcards -// func (m *SubMux) Route(basePathPattern string, sr Subrouter) { -func (m *SubMux) Route(pattern string, sr Subrouter) { - log := m.log.With("method", "Route", "pattern", pattern, "sr", sr) - defer log.DebugCallf("Route")() - method, segments, _ := getHTTPMethodAndPathSegments(m.log, pattern) - - if pattern == "" || (len(segments) == 0 && pattern[len(pattern)-1] != '/') { - panic("invalid subpath: " + pattern) - } - - numPatternSegments := len(segments) - - handler := m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr)) - - cleanPattern := "/" - if joinedSegments := strings.Join(segments, "/"); joinedSegments != "" { - cleanPattern = "/" + joinedSegments + "/" - } - cleanPattern = path.Clean(cleanPattern) - if method != "" { - cleanPattern = method + " " + cleanPattern - } - log.Debugf("Handle about to be called: cleanPattern: %s", cleanPattern) - // --- - - /* - // --- - trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/") - if trimmedSubpathPattern == "" && basePathPattern != "/" { - panic("invalid subpath: " + basePathPattern) - } - - var trimmedPatternSegments []string - if trimmedSubpathPattern != "" { - trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/") - } - numPatternSegments := len(trimmedPatternSegments) - - handler := m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr)) - - // TODO: we're wrapping the pattern method! - cleanPattern := "/" - if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" { - cleanPattern = "/" + joinedSegments + "/" - } - // --- - */ - - m.Handle(cleanPattern, handler) - - /* - m.Mux.Handle("GET "+cleanPattern, handler) - m.Mux.Handle("POST "+cleanPattern, handler) - m.Mux.Handle("PUT "+cleanPattern, handler) - m.Mux.Handle("PATCH "+cleanPattern, handler) - m.Mux.Handle("DELETE "+cleanPattern, handler) - */ -} - -// // Route does not accept methods or ... wildcards -// func (m *SubMux) Route(basePathPattern string, sr Subrouter) { -// trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/") -// if trimmedSubpathPattern == "" && basePathPattern != "/" { -// panic("invalid subpath: " + basePathPattern) -// } -// -// var trimmedPatternSegments []string -// if trimmedSubpathPattern != "" { -// trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/") -// } -// numPatternSegments := len(trimmedPatternSegments) -// -// handler := m.applyMiddleware(buildSubrouterHandlerFunc(numPatternSegments, sr)) -// -// // TODO: we're wrapping the pattern method! -// cleanPattern := "/" -// if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" { -// cleanPattern = "/" + joinedSegments + "/" -// } -// -// m.Handle(cleanPattern, handler) -// -// /* -// m.Mux.Handle("GET "+cleanPattern, handler) -// m.Mux.Handle("POST "+cleanPattern, handler) -// m.Mux.Handle("PUT "+cleanPattern, handler) -// m.Mux.Handle("PATCH "+cleanPattern, handler) -// m.Mux.Handle("DELETE "+cleanPattern, handler) -// */ -// } - -func buildSubrouterHandlerFunc(logger *logging.Logger, numPatternSegments int, sr Subrouter) response.HandlerFunc { - log := logger.With( - "function", "buildSubrouterHandlerFunc", - "numPatternSegments", numPatternSegments, - ) - - return func(r *http.Request) (res response.Response, err error) { - log := log.With("r.URL", r.URL) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", []any{ - "res", res, - "err", err, - } - }) - - fullPath := r.URL.Path - - // set trailing path on request - - segments, trailingSlash := getPathSegments(logger, r.URL.Path) - - /* - endsInSlash := strings.HasSuffix(r.URL.Path, "/") - segments := strings.Split(strings.Trim(r.URL.Path, "/"), "/") - */ - tail := segments[numPatternSegments:] - tailStr := "/" - if len(tail) > 0 { - tailStr += strings.Join(tail, "/") - if trailingSlash { - tailStr += "/" - } - } - - r.URL.Path = tailStr - defer func() { - // reset the request path - r.URL.Path = fullPath - }() - - // do the request - - log.Debugf("handler called: %s", r.URL) - fn, params, ok := sr.Handler(r) - log.Debugf("handler returned: url = %s, fn = %v, params = %v, ok = %v", r.URL, fn, params, ok) - if !ok { - return nil, response.NotFound().Wrap(ErrHandlerNotFound) - } - - for k, v := range params { - r.SetPathValue(k, v) - } - - res, err = fn(r) - - return res, err - } -} - -func (m *SubMux) Handler(r *http.Request) (fn response.HandlerFunc, params map[string]string, found bool) { - log := m.log.With( - "method", "Handler", - "r.URL", r.URL, - ) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", []any{ - "fn", fn, - "params", params, - "found", found, - } - }) - return m.tree.get(r.Method, r.URL.Path) -} - -func (m *SubMux) applyMiddleware(fn response.HandlerFunc) response.HandlerFunc { - return applyMiddleware(fn, m.middleware...) -} - -// path pattern matching tree implementation - -type ( - muxTree struct { - log *logging.Logger - - branches map[string]*muxTree - wildcardKey string - wildcardBranch *muxTree - - method string - handler response.HandlerFunc - subrouteHandler response.HandlerFunc - } -) - -func newMuxTree(log *logging.Logger) *muxTree { - return &muxTree{ - log: log, - branches: make(map[string]*muxTree), - } -} - -func (m *muxTree) String() string { - if m == nil { - return "" - } - - kvs := make([]string, 0, 6) - - if m.subrouteHandler != nil { - kvs = append(kvs, fmt.Sprintf("subrouteHandler: %v", m.subrouteHandler)) - } - if m.method != "" { - kvs = append(kvs, fmt.Sprintf("method: %v", m.method)) - } - if m.handler != nil { - kvs = append(kvs, fmt.Sprintf("handler: %v", m.handler)) - } - if m.wildcardKey != "" { - kvs = append(kvs, fmt.Sprintf("wildcardKey: %v", m.wildcardKey)) - } - if m.wildcardBranch != nil { - kvs = append(kvs, fmt.Sprintf("wildcardBranch: %v", m.wildcardBranch)) - } - if len(m.branches) > 0 { - branchKvs := make([]string, 0, len(m.branches)) - for k, v := range m.branches { - branchKvs = append(branchKvs, fmt.Sprintf("%q: %s", k, v)) - } - - slices.Sort(branchKvs) - - kvs = append(kvs, fmt.Sprintf("branches: {%s}", strings.Join(branchKvs, ", "))) - } - - return fmt.Sprintf("{%s}", strings.Join(kvs, ", ")) -} - -func (m *muxTree) set(pattern string, fn response.HandlerFunc) { - log := m.log.With( - "method", "set", - "patter", pattern, - "fn", fn, - ) - defer log.DebugCallf("called")() - method, segments, trailingSlash, endOfURLWildcard := getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(m.log, pattern) - m.setBySegments( - method, - segments, - trailingSlash, - endOfURLWildcard, - fn, - ) -} - -// TODO: handle /{$} properly -func (m *muxTree) setBySegments(method string, segments []string, trailingSlash, endOfURLWildcard bool, fn response.HandlerFunc) { - log := m.log.With( - "method", "setBySegments", - "arg.method", method, - "segments", segments, - "trailingSlash", trailingSlash, - "endOfURLWildcard", endOfURLWildcard, - "fn", fn, - ) - defer log.DebugCallf("called")() - if len(segments) == 0 { - m.method = method - if !trailingSlash || endOfURLWildcard { - m.handler = fn - } else if trailingSlash { - m.subrouteHandler = fn - } - return - } - - head := segments[0] - tail := segments[1:] - - var sub *muxTree - if key, ok := getWildcardPathSegmentKey(head); ok { - m.wildcardKey = key - if sub = m.wildcardBranch; sub == nil { - sub = newMuxTree(m.log) - m.wildcardBranch = sub - } - } else { - if sub = m.branches[head]; sub == nil { - sub = newMuxTree(m.log) - m.branches[head] = sub - } - } - - /* - if isWildcard, wildcardKey := isWildcardPathSegment(head); wildcardKey == "$" { - // TODO: test - // TODO: handle trailing end path matching - if len(tail) > 0 || trailingSlash { - panic("{$} wildcard applied outside of end of path") - } - m.method = method - m.handler = fn - return - } else if isWildcard { - m.wildcardKey = wildcardKey - if sub = m.wildcardBranch; sub == nil { - sub = newMuxTree(m.log) - m.wildcardBranch = sub - } - } else { - if sub = m.branches[head]; sub == nil { - sub = newMuxTree(m.log) - m.branches[head] = sub - } - } - */ - - sub.setBySegments(method, tail, trailingSlash, endOfURLWildcard, fn) -} - -func (m *muxTree) get(method, pattern string) (fn response.HandlerFunc, params map[string]string, found bool) { - log := m.log.With( - "method", "get", - "arg.method", method, - "pattern", pattern, - ) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", []any{ - "fn", fn, - "params", params, - "found", found, - } - }) - segments, trailingSlash := getPathSegments(m.log, pattern) - return m.getByHTTPMethodAndSegments(method, segments, trailingSlash) -} - -func (m *muxTree) getByHTTPMethodAndSegments(method string, segments []string, trailingSlash bool) (fn response.HandlerFunc, params map[string]string, found bool) { - log := m.log.With( - "method", "getByHTTPMethodAndSegments", - "arg.method", method, - "segments", segments, - "trailingSlash", trailingSlash, - "muxTree", m, - ) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", []any{ - "fn", fn, - "params", params, - "found", found, - } - }) - if len(segments) == 0 { - log.Debugf("no segments") - if methodMatches := m.method == "" || m.method == method; methodMatches { - if fn = m.handler; fn == nil { - fn = m.subrouteHandler - } - return fn, map[string]string{}, fn != nil - } - return nil, map[string]string{}, false - } - - head := segments[0] - tail := segments[1:] - - if m.subrouteHandler != nil { - defer func() { - if !found && (m.method == "" || m.method == method) { - fn = m.subrouteHandler - found = true - } - }() - } - - if sm, ok := m.branches[head]; ok { - log.Debugf("matching branch found") - //return sm.getByHTTPMethodAndSegments(method, tail, trailingSlash) - if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found { - return fn, params, found - } - log.Debugf("matching branch mismatched at subpath") - } else { - log.Debugf("no matching branch found") - } - if sm := m.wildcardBranch; sm != nil { - wlog := log.With( - "wildcardKey", m.wildcardKey, - "sub.muxTree", sm, - ) - wlog.Debugf("wildcard branch found") - //fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash) - if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found { - params[m.wildcardKey] = head - return fn, params, found - } - wlog.Debugf("wildcard branch mismatched at subpath") - } else { - log.Debugf("no wildcard branch found") - } - - // --- TODO: test --- - if m.subrouteHandler != nil && (m.method == method || m.method == "") { - log.Debugf("subroutes captured") - return m.subrouteHandler, nil, true - } - - log.Debugf("subroutes not captured") - // --- - - return nil, map[string]string{}, false -} - -func getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(logger *logging.Logger, pattern string) (method string, segments []string, trailingSlash, endOfURLWildcard bool) { - log := logger.With( - "function", "getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard", - "pattern", pattern, - ) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", []any{ - "method", method, - "segments", segments, - "trailingSlash", trailingSlash, - "endOfURLWildcard", endOfURLWildcard, - } - }) - if vs := strings.SplitN(pattern, " ", 2); len(vs) == 2 { - method = vs[0] - pattern = vs[1] - } - segments, trailingSlash, endOfURLWildcard = getPathSegmentsWithoutEndOfURLWildcard(logger, pattern) - - return method, segments, trailingSlash, endOfURLWildcard -} - -func getHTTPMethodAndPathSegments(logger *logging.Logger, pattern string) (method string, segments []string, trailingSlash bool) { - log := logger.With( - "function", "getHTTPMethodAndPathSegments", - "pattern", pattern, - ) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", []any{ - "method", method, - "segments", segments, - "trailingSlash", trailingSlash, - } - }) - if vs := strings.SplitN(pattern, " ", 2); len(vs) == 2 { - method = vs[0] - pattern = vs[1] - } - segments, trailingSlash = getPathSegments(logger, pattern) - - return method, segments, trailingSlash -} - -func getPathSegments(logger *logging.Logger, pathPattern string) (segments []string, trailingSlash bool) { - log := logger.With( - "function", "getPathSegments", - "pathPattern", pathPattern, - ) - defer log.DebugCallf("getPathSegments")() - trailingSlash = strings.HasSuffix(pathPattern, "/") - - p := strings.Trim(path.Clean(pathPattern), "/") - if p == "" { - return nil, trailingSlash - } - - return strings.Split(p, "/"), trailingSlash -} - -// NOTE: if endOfURLWildcard, then trailingSlash -func getPathSegmentsWithoutEndOfURLWildcard(logger *logging.Logger, pathPattern string) (segments []string, trailingSlash, endOfURLWildcard bool) { - log := logger.With( - "function", "getPathSegmentsWithoutEndOfURLWildcard", - "pathPattern", pathPattern, - ) - defer log.DebugDeferf("called")(func() (string, []any) { - return "returned", []any{ - "segments", segments, - "trailingSlash", trailingSlash, - "endOfURLWildcard", endOfURLWildcard, - } - }) - - endOfURLWildcard = strings.HasSuffix(pathPattern, "/{$}") - if endOfURLWildcard { - pathPattern = pathPattern[:len(pathPattern)-3] - } - - trailingSlash = strings.HasSuffix(pathPattern, "/") - - p := strings.Trim(path.Clean(pathPattern), "/") - if p == "" { - return nil, trailingSlash, endOfURLWildcard - } - - return strings.Split(p, "/"), trailingSlash, endOfURLWildcard -} - -func getWildcardPathSegmentKey(s string) (string, bool) { - if isWildcard := len(s) > 2 && s[0] == '{' && s[len(s)-1] == '}'; isWildcard { - return s[1 : len(s)-1], true - } - return "", false -} - -func applyMiddleware(fn response.HandlerFunc, ms ...response.Middleware) response.HandlerFunc { - for _, mw := range slices.Backward(ms) { - prev := fn - fn = mw(func(r *http.Request) (response.Response, error) { - return prev(r) - }) - } - - return fn -} - -// TODO: remove the consts. trace level diff --git a/internal/server/server.go b/internal/server/server.go index 0c55808..c8353dd 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,14 +3,15 @@ package server import ( "context" "encoding/json" - "errors" "fmt" "html/template" "net/http" "path" "strconv" + "strings" "github.com/angelbeltran/templater" + "github.com/gin-gonic/gin" "ruben/inventory2/internal/domains/accounts" "ruben/inventory2/internal/domains/authentication" @@ -23,11 +24,11 @@ import ( "ruben/inventory2/internal/server/api/webhooks" etsy_webhooks "ruben/inventory2/internal/server/api/webhooks/etsy" "ruben/inventory2/internal/server/middleware" - "ruben/inventory2/internal/server/response" - "ruben/inventory2/internal/server/router" ) -func NewServer( +// r gin.IRouter, // TODO: use this everywhere + +func Router( ctx context.Context, logger *logging.Logger, contentDir string, @@ -35,11 +36,8 @@ func NewServer( accts *accounts.Store, etsy *etsy_platform.Platform, auth *authentication.Authenticator, -) http.Handler { - mux := router.NewMux( - logger, - middleware.LogRequests(ctx, logger.WithGroup("request")), - ) +) *gin.Engine { + r := gin.Default() // TODO: shouldn't this ACTUALLY be a middleware? // - only try to make this an actual middleware AFTER all the routers are broken out, so that way how the middleware is supposed to work can be known @@ -54,22 +52,26 @@ func NewServer( // non-html content: scripts, styles, images, etc - scfs := http.FileServer(http.Dir(contentDir + "/scripts")) - mux.Mux.Handle("GET /scripts/", http.StripPrefix("/scripts", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Use(fileServer("/scripts", contentDir+"/scripts", func(c *gin.Context) { + w := c.Writer w.Header().Set("Content-Type", "text/javascript") - if path.Ext(r.URL.Path) == ".gz" { + if path.Ext(c.Request.URL.Path) == ".gz" { w.Header().Set("Content-Encoding", "gzip") } - scfs.ServeHTTP(w, r) - }))) - mux.Mux.Handle("GET /styles/", http.StripPrefix("/styles", http.FileServer(http.Dir(contentDir+"/styles")))) - mux.Mux.Handle("GET /favicon/", http.StripPrefix("/favicon", http.FileServer(http.Dir(contentDir+"/favicon")))) + })) + r.Static("/styles", "./styles") + r.Static("/favicon", "./favicon") // html (must be before the api endpoints, because the webpage middleware has to be installed beforehand + r.GET("/", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/ui") + }) + // kind of a dumb way to capture routes for webpages - wpr := templates_api.NewWebpageRouter( + templates_api.SetupRoutes( logger.WithGroup("templates"), + r.Group("/ui"), contentDir, templater.NewTemplater( contentDir+"/templates", @@ -125,42 +127,27 @@ func NewServer( etsy, authMiddleware, ) - mux.AddMiddleware( - func(fn response.HandlerFunc) response.HandlerFunc { - return func(r *http.Request) (response.Response, error) { - res, err := fn(r) - if err == nil || !errors.Is(err, router.ErrHandlerNotFound) { - return res, err - } - - renderWebPage, pathParams, found := wpr.Handler(r) - if !found { - return nil, response.NotFound() - } - - for k, v := range pathParams { - r.SetPathValue(k, v) - } - - return renderWebPage(r) - } - }, - ) - - mux.Route("/", wpr) // api endpoints - mux.Route("/auth", auth_api.NewLoginSubrouter(logger.WithGroup("/auth"), auth)) - mux.Route("/accounts", accounts_api.NewAccountSubrouter( + api := r.Group("/api") + + auth_api.Routes( + api.Group("/auth"), + logger.WithGroup("/auth"), + auth, + ) + accounts_api.Routes( + api.Group("/accounts"), logger.WithGroup("/accounts"), accts, authMiddleware, - )) + ) // api webhooks (TODO: make a router for these) - webhookHandler := http.StripPrefix("/webhooks", webhooks.New( + webhooks.Webhooks( + api.Group("/webhooks"), logger.WithGroup("webhooks"), rawEvents, etsy, @@ -169,10 +156,23 @@ func NewServer( OAuthRedirectURIWithAcctIDParam: "/oauth/account/{acctID}/auth_code", }, }, - )) - mux.Mux.Handle("GET /webhooks/", webhookHandler) - mux.Mux.Handle("POST /webhooks/", webhookHandler) - mux.Mux.Handle("PUT /webhooks/", webhookHandler) + ) - return mux + return r +} + +func fileServer(urlPrefix, dir string, beforeServe func(c *gin.Context)) gin.HandlerFunc { + scfs := http.StripPrefix(urlPrefix, http.FileServer(http.Dir(dir))) + return func(c *gin.Context) { + r := c.Request + if r.URL.Path == urlPrefix || strings.HasPrefix(r.URL.Path, path.Join(urlPrefix, "/")) { + if beforeServe != nil { + beforeServe(c) + } + scfs.ServeHTTP(c.Writer, r) + c.Abort() + } + + // TODO: need a c.Next()? + } } diff --git a/main.go b/main.go index b3076ef..bddc2e9 100644 --- a/main.go +++ b/main.go @@ -50,24 +50,6 @@ func main() { // Time format (Default: time.StampMilli) //TimeFormat: "", })) - /* - logger := slog.New(tint.NewHandler(os.Stderr, &tint.Options{ - AddSource: true, - Level: slog.LevelDebug, - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - // this can perform general key=value log cleanup - - if a.Key == slog.SourceKey && len(groups) == 0 { - source := a.Value.Any().(*slog.Source) - source.File = strings.TrimPrefix(source.File, "/home/angel/go/src/ruben/inventory2/internal") - } - - return a - }, - // Time format (Default: time.StampMilli) - //TimeFormat: "", - })) - */ logger.Info("application starting") @@ -168,25 +150,27 @@ func runAuthProcesses(ctx context.Context, auth *authentication.Authenticator) < } func runServer(ctx context.Context, logger *logging.Logger, connPool *pgxpool.Pool, auth *authentication.Authenticator) <-chan error { - srv := &http.Server{ - Addr: ":8082", // local - Handler: server.NewServer( - ctx, - logger.WithGroup("server"), - "./", - raw_events.NewStore(logger.WithGroup("raw-event-store"), connPool), - accounts.NewStore(logger, connPool), - etsy_platform.NewPlatform( - logger, - func(acctID int64) string { - return fmt.Sprintf("/oauth/account/%d/auth_code", acctID) - }, - etsyAPIKeystring, - etsyAPISharedSecret, - connPool, - ), - auth, + r := server.Router( + ctx, + logger.WithGroup("server"), + "./", + raw_events.NewStore(logger.WithGroup("raw-event-store"), connPool), + accounts.NewStore(logger, connPool), + etsy_platform.NewPlatform( + logger, + func(acctID int64) string { + return fmt.Sprintf("/oauth/account/%d/auth_code", acctID) + }, + etsyAPIKeystring, + etsyAPISharedSecret, + connPool, ), + auth, + ) + + srv := &http.Server{ + Addr: ":8082", // local + Handler: r, } ctx, cancel := context.WithCancel(ctx) diff --git a/templates/component_bodies/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}.html.tmpl b/templates/component_bodies/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}.html.tmpl index 0219382..923381f 100644 --- a/templates/component_bodies/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}.html.tmpl +++ b/templates/component_bodies/accounts/{acctID}/inventory/sync-groups/draft/listings/{orderIndex}.html.tmpl @@ -33,7 +33,7 @@ {{- $shops := $dot.Accounts.GetShops $acctID -}}