first drafts for inventory sync page
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
@@ -46,6 +46,7 @@ entity "**oauth_login_states**" {
|
|||||||
+ ""state"": //bytea [PK]//
|
+ ""state"": //bytea [PK]//
|
||||||
--
|
--
|
||||||
*""expiration"": //timestamp with time zone //
|
*""expiration"": //timestamp with time zone //
|
||||||
|
*""target_uri"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
entity "**oauth_tokens**" {
|
entity "**oauth_tokens**" {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ require (
|
|||||||
github.com/alecthomas/kingpin v2.2.6+incompatible // indirect
|
github.com/alecthomas/kingpin v2.2.6+incompatible // indirect
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect
|
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/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
|
||||||
github.com/getkin/kin-openapi v0.133.0 // indirect
|
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||||
@@ -26,6 +27,7 @@ require (
|
|||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // 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/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
@@ -40,14 +42,16 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/speakeasy-api/jsonpath v0.6.0 // indirect
|
github.com/speakeasy-api/jsonpath v0.6.0 // indirect
|
||||||
github.com/speakeasy-api/openapi-overlay v0.10.2 // 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/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||||
golang.org/x/crypto v0.46.0 // indirect
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
golang.org/x/mod v0.30.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.39.0 // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
@@ -56,5 +60,6 @@ require (
|
|||||||
|
|
||||||
tool (
|
tool (
|
||||||
github.com/achiku/planter
|
github.com/achiku/planter
|
||||||
|
github.com/angelbeltran/concurry
|
||||||
github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
|
github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ 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/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 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
|
||||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
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 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
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/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
@@ -15,6 +23,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
|
|||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/coreos/go-oidc/v3 v3.8.0 h1:s3e30r6VEl3/M7DTSCEuImmrfu1/1WBgA0cXkdzkrAY=
|
github.com/coreos/go-oidc/v3 v3.8.0 h1:s3e30r6VEl3/M7DTSCEuImmrfu1/1WBgA0cXkdzkrAY=
|
||||||
github.com/coreos/go-oidc/v3 v3.8.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
|
github.com/coreos/go-oidc/v3 v3.8.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -58,6 +67,8 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
|||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -113,12 +124,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8=
|
github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8=
|
||||||
github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
|
github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
|
||||||
github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
|
github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
|
||||||
github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
|
github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
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/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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@@ -136,6 +152,7 @@ github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIj
|
|||||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -147,6 +164,8 @@ 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.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 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
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=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -199,6 +218,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
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 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
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=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package accounts
|
package accounts
|
||||||
|
|
||||||
|
// to generate StoreWithContext
|
||||||
|
//go:generate concurry -s Store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -15,20 +18,51 @@ type (
|
|||||||
db *pgxpool.Pool
|
db *pgxpool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
StoreWithContext struct {
|
|
||||||
ctx context.Context
|
|
||||||
db *Store
|
|
||||||
}
|
|
||||||
|
|
||||||
Account struct {
|
Account struct {
|
||||||
UserID string
|
AccountIDs
|
||||||
ID int64
|
OAuthUser
|
||||||
Email string
|
Email string
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthUser struct {
|
OAuthUser struct {
|
||||||
UserID string
|
UserID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountShop struct {
|
||||||
|
AccountShopIDs
|
||||||
|
Platform Platform
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
Listing struct {
|
||||||
|
AccountIDs
|
||||||
|
Platform Platform
|
||||||
|
ShopID string
|
||||||
|
ListingID int64
|
||||||
|
SKU string
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ids
|
||||||
|
|
||||||
|
AccountIDs struct {
|
||||||
|
AccountID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountShopIDs struct {
|
||||||
|
AccountIDs
|
||||||
|
ShopID string
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Etsy Platform = "Etsy"
|
||||||
|
Tiktok Platform = "Tiktok"
|
||||||
|
Wix Platform = "Wix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewStore(db *pgxpool.Pool) *Store {
|
func NewStore(db *pgxpool.Pool) *Store {
|
||||||
@@ -38,10 +72,7 @@ func NewStore(db *pgxpool.Pool) *Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *Store) WithContext(ctx context.Context) *StoreWithContext {
|
func (db *Store) WithContext(ctx context.Context) *StoreWithContext {
|
||||||
return &StoreWithContext{
|
return NewStoreWithContext(ctx, db)
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Account, error) {
|
func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Account, error) {
|
||||||
@@ -78,8 +109,12 @@ func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Accou
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Account{
|
return Account{
|
||||||
|
OAuthUser: OAuthUser{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ID: acctID,
|
},
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: acctID,
|
||||||
|
},
|
||||||
Email: email,
|
Email: email,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -87,7 +122,7 @@ func (db *Store) CreateAccount(ctx context.Context, userID, email string) (Accou
|
|||||||
func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) {
|
func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) {
|
||||||
rows, err := db.db.Query(
|
rows, err := db.db.Query(
|
||||||
ctx,
|
ctx,
|
||||||
"SELECT email FROM accounts WHERE account_id = @account_id",
|
"SELECT email, user_id FROM accounts WHERE account_id = @account_id",
|
||||||
pgx.NamedArgs{
|
pgx.NamedArgs{
|
||||||
"account_id": id,
|
"account_id": id,
|
||||||
},
|
},
|
||||||
@@ -96,7 +131,12 @@ func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) {
|
|||||||
return Account{}, fmt.Errorf("failed to perform query: %w", err)
|
return Account{}, fmt.Errorf("failed to perform query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
email, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[string])
|
type Row struct {
|
||||||
|
Email string
|
||||||
|
User_id string
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return Account{}, consts.ErrNotFound
|
return Account{}, consts.ErrNotFound
|
||||||
@@ -106,8 +146,13 @@ func (db *Store) GetAccount(ctx context.Context, id int64) (Account, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Account{
|
return Account{
|
||||||
ID: id,
|
AccountIDs: AccountIDs{
|
||||||
Email: email,
|
AccountID: id,
|
||||||
|
},
|
||||||
|
OAuthUser: OAuthUser{
|
||||||
|
r.User_id,
|
||||||
|
},
|
||||||
|
Email: r.Email,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,8 +188,12 @@ func (db *Store) GetAccountByUserID(ctx context.Context, userID string) (Account
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Account{
|
return Account{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: r.Account_ID,
|
||||||
|
},
|
||||||
|
OAuthUser: OAuthUser{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
ID: r.Account_ID,
|
},
|
||||||
Email: r.Email,
|
Email: r.Email,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -152,7 +201,7 @@ func (db *Store) GetAccountByUserID(ctx context.Context, userID string) (Account
|
|||||||
func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, error) {
|
func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account, error) {
|
||||||
rows, err := db.db.Query(
|
rows, err := db.db.Query(
|
||||||
ctx,
|
ctx,
|
||||||
"SELECT account_id FROM accounts WHERE email = @email",
|
"SELECT account_id, user_id FROM accounts WHERE email = @email",
|
||||||
pgx.NamedArgs{
|
pgx.NamedArgs{
|
||||||
"email": email,
|
"email": email,
|
||||||
},
|
},
|
||||||
@@ -161,7 +210,12 @@ func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account,
|
|||||||
return Account{}, fmt.Errorf("failed to perform query: %w", err)
|
return Account{}, fmt.Errorf("failed to perform query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64])
|
type Row struct {
|
||||||
|
Account_id int64
|
||||||
|
User_id string
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[Row])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return Account{}, consts.ErrNotFound
|
return Account{}, consts.ErrNotFound
|
||||||
@@ -171,7 +225,12 @@ func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Account{
|
return Account{
|
||||||
ID: id,
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: r.Account_id,
|
||||||
|
},
|
||||||
|
OAuthUser: OAuthUser{
|
||||||
|
UserID: r.User_id,
|
||||||
|
},
|
||||||
Email: email,
|
Email: email,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -213,28 +272,53 @@ func (db *Store) GetUserAndAccountByAccessToken(ctx context.Context, accessToken
|
|||||||
return OAuthUser{}, nil, fmt.Errorf("failed to scan row: %w", err)
|
return OAuthUser{}, nil, fmt.Errorf("failed to scan row: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user := OAuthUser{UserID: r.User_ID}
|
||||||
|
|
||||||
var acct *Account
|
var acct *Account
|
||||||
if r.Account_ID != nil {
|
if r.Account_ID != nil {
|
||||||
acct = &Account{
|
acct = &Account{
|
||||||
UserID: r.User_ID,
|
AccountIDs: AccountIDs{
|
||||||
ID: *r.Account_ID,
|
AccountID: *r.Account_ID,
|
||||||
|
},
|
||||||
|
OAuthUser: user,
|
||||||
Email: *r.Email,
|
Email: *r.Email,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return OAuthUser{UserID: r.User_ID}, acct, nil
|
return user, acct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *StoreWithContext) CreateAccount(userID, email string) (Account, error) {
|
func (db *Store) GetShops(ctx context.Context, acctID int64) ([]AccountShop, error) {
|
||||||
return db.db.CreateAccount(db.ctx, userID, email)
|
var shops []AccountShop
|
||||||
|
for _, v := range devShops {
|
||||||
|
if v.AccountID != acctID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
shops = append(shops, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *StoreWithContext) GetAccount(id int64) (Account, error) {
|
return shops, nil
|
||||||
return db.db.GetAccount(db.ctx, id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) {
|
func (db *Store) GetListingsForShop(ctx context.Context, acctID int64, shopID string) ([]Listing, error) {
|
||||||
acct, err := db.db.GetAccountByUserID(db.ctx, userID)
|
var vs []Listing
|
||||||
|
for _, v := range devListings {
|
||||||
|
if v.AccountID != acctID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.ShopID != shopID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vs = append(vs, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// additional context
|
||||||
|
|
||||||
|
func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (*Account, error) {
|
||||||
|
acct, err := db.GetAccountByUserID(ctx, userID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return &acct, nil
|
return &acct, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package accounts
|
||||||
|
|
||||||
|
var (
|
||||||
|
devShops = []AccountShop{
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "2",
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
Name: "Etsy 1",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "5",
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
Name: "Etsy 2",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "8",
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
Name: "Etsy 3",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "11",
|
||||||
|
},
|
||||||
|
Platform: Tiktok,
|
||||||
|
Name: "Tiktok 1",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "14",
|
||||||
|
},
|
||||||
|
Platform: Tiktok,
|
||||||
|
Name: "Tiktok 2",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "17",
|
||||||
|
},
|
||||||
|
Platform: Tiktok,
|
||||||
|
Name: "Tiktok 3",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "20",
|
||||||
|
},
|
||||||
|
Platform: Wix,
|
||||||
|
Name: "Wix 1",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "23",
|
||||||
|
},
|
||||||
|
Platform: Wix,
|
||||||
|
Name: "Wix 2",
|
||||||
|
},
|
||||||
|
AccountShop{
|
||||||
|
AccountShopIDs: AccountShopIDs{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
ShopID: "26",
|
||||||
|
},
|
||||||
|
Platform: Wix,
|
||||||
|
Name: "Wix 3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
devListings = []Listing{
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
ShopID: "1",
|
||||||
|
ListingID: 1,
|
||||||
|
SKU: "sku 1",
|
||||||
|
Name: "name 1",
|
||||||
|
Description: "description 1",
|
||||||
|
Count: 51,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
ShopID: "1",
|
||||||
|
ListingID: 2,
|
||||||
|
SKU: "sku 2",
|
||||||
|
Name: "name 2",
|
||||||
|
Description: "description 2",
|
||||||
|
Count: 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
ShopID: "1",
|
||||||
|
ListingID: 3,
|
||||||
|
SKU: "sku 3",
|
||||||
|
Name: "name 3",
|
||||||
|
Description: "description 3",
|
||||||
|
Count: 53,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Tiktok,
|
||||||
|
ShopID: "4",
|
||||||
|
ListingID: 4,
|
||||||
|
SKU: "sku 4",
|
||||||
|
Name: "name 4",
|
||||||
|
Description: "description 4",
|
||||||
|
Count: 54,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Tiktok,
|
||||||
|
ShopID: "4",
|
||||||
|
ListingID: 5,
|
||||||
|
SKU: "sku 5",
|
||||||
|
Name: "name 5",
|
||||||
|
Description: "description 5",
|
||||||
|
Count: 55,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Tiktok,
|
||||||
|
ShopID: "4",
|
||||||
|
ListingID: 6,
|
||||||
|
SKU: "sku 6",
|
||||||
|
Name: "name 6",
|
||||||
|
Description: "description 6",
|
||||||
|
Count: 56,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Wix,
|
||||||
|
ShopID: "7",
|
||||||
|
ListingID: 7,
|
||||||
|
SKU: "sku 7",
|
||||||
|
Name: "name 7",
|
||||||
|
Description: "description 7",
|
||||||
|
Count: 57,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Wix,
|
||||||
|
ShopID: "7",
|
||||||
|
ListingID: 8,
|
||||||
|
SKU: "sku 8",
|
||||||
|
Name: "name 8",
|
||||||
|
Description: "description 8",
|
||||||
|
Count: 58,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Wix,
|
||||||
|
ShopID: "7",
|
||||||
|
ListingID: 9,
|
||||||
|
SKU: "sku 9",
|
||||||
|
Name: "name 9",
|
||||||
|
Description: "description 9",
|
||||||
|
Count: 59,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
ShopID: "2",
|
||||||
|
ListingID: 10,
|
||||||
|
SKU: "sku 10",
|
||||||
|
Name: "name 10",
|
||||||
|
Description: "description 10",
|
||||||
|
Count: 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
ShopID: "2",
|
||||||
|
ListingID: 11,
|
||||||
|
SKU: "sku 11",
|
||||||
|
Name: "name 11",
|
||||||
|
Description: "description 11",
|
||||||
|
Count: 61,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AccountIDs: AccountIDs{
|
||||||
|
AccountID: 6,
|
||||||
|
},
|
||||||
|
Platform: Etsy,
|
||||||
|
ShopID: "2",
|
||||||
|
ListingID: 12,
|
||||||
|
SKU: "sku 12",
|
||||||
|
Name: "name 12",
|
||||||
|
Description: "description 12",
|
||||||
|
Count: 62,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Code generated by concurry DO NOT EDIT.
|
||||||
|
// https://github.com/angelbeltran/concurry
|
||||||
|
// concurry
|
||||||
|
package accounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreWithContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
*Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStoreWithContext(ctx context.Context, v *Store) *StoreWithContext {
|
||||||
|
return &StoreWithContext{
|
||||||
|
ctx: ctx,
|
||||||
|
Store: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) CreateAccount(userID string, email string) (Account, error) {
|
||||||
|
return v_ctx.Store.CreateAccount(v_ctx.ctx, userID, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetAccount(id int64) (Account, error) {
|
||||||
|
return v_ctx.Store.GetAccount(v_ctx.ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetAccountByUserID(userID string) (Account, error) {
|
||||||
|
return v_ctx.Store.GetAccountByUserID(v_ctx.ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetAccountByEmail(email string) (Account, error) {
|
||||||
|
return v_ctx.Store.GetAccountByEmail(v_ctx.ctx, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetUserAndAccountByAccessToken(accessToken string) (OAuthUser, *Account, error) {
|
||||||
|
return v_ctx.Store.GetUserAndAccountByAccessToken(v_ctx.ctx, accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetShops(acctID int64) ([]AccountShop, error) {
|
||||||
|
return v_ctx.Store.GetShops(v_ctx.ctx, acctID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetListingsForShop(acctID int64, shopID string) ([]Listing, error) {
|
||||||
|
return v_ctx.Store.GetListingsForShop(v_ctx.ctx, acctID, shopID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) {
|
||||||
|
return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID)
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
//go:generate oapi-codegen -generate types,client -package generated_client -o generated_client/client.go openapi.3.0.2.json
|
//go:generate oapi-codegen -generate types,client -package generated_client -o generated_client/client.go openapi.3.0.2.json
|
||||||
|
//go:generate concurry -s Platform
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - [x] document the flow in the README.md
|
// - [x] document the flow in the README.md
|
||||||
@@ -36,11 +37,6 @@ type (
|
|||||||
apiSharedSecret string
|
apiSharedSecret string
|
||||||
db *pgxpool.Pool
|
db *pgxpool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
PlatformWithContext struct {
|
|
||||||
ctx context.Context
|
|
||||||
p *Platform
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// oauth scopes
|
// oauth scopes
|
||||||
@@ -77,10 +73,7 @@ func NewPlatform(oAuthRedirectURI func(acctID int64) string, apiKeystring, apiSh
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Platform) WithContext(ctx context.Context) *PlatformWithContext {
|
func (p *Platform) WithContext(ctx context.Context) *PlatformWithContext {
|
||||||
return &PlatformWithContext{
|
return NewPlatformWithContext(ctx, p)
|
||||||
ctx: ctx,
|
|
||||||
p: p,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Platform) GenerateConnectionURLForNewAccount(ctx context.Context, acctID int64) (*url.URL, error) {
|
func (p *Platform) GenerateConnectionURLForNewAccount(ctx context.Context, acctID int64) (*url.URL, error) {
|
||||||
@@ -113,10 +106,6 @@ func (p *Platform) GenerateConnectionURLForNewAccount(ctx context.Context, acctI
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlatformWithContext) GenerateConnectionURLForNewAccount(acctID int64) (*url.URL, error) {
|
|
||||||
return p.p.GenerateConnectionURLForNewAccount(p.ctx, acctID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleNewAuthCode handles the auth code to get api access
|
// HandleNewAuthCode handles the auth code to get api access
|
||||||
func (p *Platform) HandleNewAuthCode(ctx context.Context, acctID int64, state, authCode string) (bool, error) {
|
func (p *Platform) HandleNewAuthCode(ctx context.Context, acctID int64, state, authCode string) (bool, error) {
|
||||||
// look up matching oauth request
|
// look up matching oauth request
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Code generated by concurry DO NOT EDIT.
|
||||||
|
// https://github.com/angelbeltran/concurry
|
||||||
|
// concurry
|
||||||
|
package etsy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlatformWithContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
*Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlatformWithContext(ctx context.Context, v *Platform) *PlatformWithContext {
|
||||||
|
return &PlatformWithContext{
|
||||||
|
ctx: ctx,
|
||||||
|
Platform: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) GenerateConnectionURLForNewAccount(acctID int64) (*url.URL, error) {
|
||||||
|
return v_ctx.Platform.GenerateConnectionURLForNewAccount(v_ctx.ctx, acctID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) HandleNewAuthCode(acctID int64, state string, authCode string) (bool, error) {
|
||||||
|
return v_ctx.Platform.HandleNewAuthCode(v_ctx.ctx, acctID, state, authCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) parseAccessCodeResponseBody(body []byte) (string, string, time.Time, int64, error) {
|
||||||
|
return v_ctx.Platform.parseAccessCodeResponseBody(v_ctx.ctx, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) getNewUserShopID(accessToken string, userID int64) (int64, error) {
|
||||||
|
return v_ctx.Platform.getNewUserShopID(v_ctx.ctx, accessToken, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) GetUserPointerByAccountID(acctID int64) (*EtsyUser, error) {
|
||||||
|
return v_ctx.Platform.GetUserPointerByAccountID(v_ctx.ctx, acctID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) saveNewEtsyUser(user EtsyUser, tokens etsyAccessTokens) error {
|
||||||
|
return v_ctx.Platform.saveNewEtsyUser(v_ctx.ctx, user, tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) createNewOAuthRequest(acctID int64) (oauth2Request, error) {
|
||||||
|
return v_ctx.Platform.createNewOAuthRequest(v_ctx.ctx, acctID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) getOauthRequest(state uuid.UUID) (oauth2Request, bool, error) {
|
||||||
|
return v_ctx.Platform.getOauthRequest(v_ctx.ctx, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) InvalidateState(state string) error {
|
||||||
|
return v_ctx.Platform.InvalidateState(v_ctx.ctx, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *PlatformWithContext) deleteOauthRequest(state uuid.UUID) error {
|
||||||
|
return v_ctx.Platform.deleteOauthRequest(v_ctx.ctx, state)
|
||||||
|
}
|
||||||
@@ -73,10 +73,6 @@ func (p *Platform) GetUserPointerByAccountID(ctx context.Context, acctID int64)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlatformWithContext) GetUserPointerByAccountID(acctID int64) (*EtsyUser, error) {
|
|
||||||
return p.p.GetUserPointerByAccountID(p.ctx, acctID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Platform) saveNewEtsyUser(
|
func (p *Platform) saveNewEtsyUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
user EtsyUser,
|
user EtsyUser,
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ type (
|
|||||||
db *pgxpool.Pool
|
db *pgxpool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoreWithContext allows currying all Store methods by context.Context
|
|
||||||
StoreWithContext struct {
|
|
||||||
ctx context.Context
|
|
||||||
db *Store
|
|
||||||
}
|
|
||||||
|
|
||||||
Event struct {
|
Event struct {
|
||||||
Platform string
|
Platform string
|
||||||
StoreID string
|
StoreID string
|
||||||
@@ -38,10 +32,7 @@ func NewStore(db *pgxpool.Pool) *Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *Store) WithContext(ctx context.Context) *StoreWithContext {
|
func (db *Store) WithContext(ctx context.Context) *StoreWithContext {
|
||||||
return &StoreWithContext{
|
return NewStoreWithContext(ctx, db)
|
||||||
ctx: ctx,
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Store) Save(ctx context.Context, e *Event) error {
|
func (db *Store) Save(ctx context.Context, e *Event) error {
|
||||||
@@ -81,10 +72,6 @@ func (db *Store) Save(ctx context.Context, e *Event) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *StoreWithContext) Save(e *Event) error {
|
|
||||||
return db.db.Save(db.ctx, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Store) LoadEventsForStore(
|
func (db *Store) LoadEventsForStore(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
platform string,
|
platform string,
|
||||||
@@ -125,10 +112,3 @@ func (db *Store) LoadEventsForStore(
|
|||||||
|
|
||||||
return evts, nil
|
return evts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *StoreWithContext) LoadEventsForStore(
|
|
||||||
platform string,
|
|
||||||
storeID string,
|
|
||||||
) ([]Event, error) {
|
|
||||||
return db.db.LoadEventsForStore(db.ctx, platform, storeID)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Code generated by concurry DO NOT EDIT.
|
||||||
|
// https://github.com/angelbeltran/concurry
|
||||||
|
// concurry
|
||||||
|
package raw_events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreWithContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
*Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStoreWithContext(ctx context.Context, v *Store) *StoreWithContext {
|
||||||
|
return &StoreWithContext{
|
||||||
|
ctx: ctx,
|
||||||
|
Store: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) Save(e *Event) error {
|
||||||
|
return v_ctx.Store.Save(v_ctx.ctx, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) LoadEventsForStore(platform string, storeID string) ([]Event, error) {
|
||||||
|
return v_ctx.Store.LoadEventsForStore(v_ctx.ctx, platform, storeID)
|
||||||
|
}
|
||||||
@@ -28,5 +28,5 @@ func (s *Server) createAccount(r *http.Request) (response.Response, error) {
|
|||||||
return nil, response.Errorf("failed to create account: %w", err)
|
return nil, response.Errorf("failed to create account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.ID)), nil
|
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.AccountID)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
id := getIdentity(r.Context())
|
id := getIdentity(r.Context())
|
||||||
if id.Account == nil || id.Account.ID != acctID {
|
if id.Account == nil || id.Account.AccountID != acctID {
|
||||||
return response.Unauthorized().
|
return response.Unauthorized().
|
||||||
Msgf("user does not have access to account %d", acctID)
|
Msgf("user does not have access to account %d", acctID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (s *Server) login(r *http.Request) (response.Response, error) {
|
|||||||
return nil, response.Errorf("failed to create account: %w", err)
|
return nil, response.Errorf("failed to create account: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.ID)), nil
|
return response.SeeOther(fmt.Sprintf("/accounts/%d", acct.AccountID)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /login/callback
|
// GET /login/callback
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ func (b bodyRes) Redirect(code redirect.Code, to string) Response {
|
|||||||
return Redirect(code, to).wrap(b)
|
return Redirect(code, to).wrap(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b bodyRes) HTML(body []byte) Response {
|
||||||
|
return HTML(body).wrap(b)
|
||||||
|
}
|
||||||
|
|
||||||
func (b bodyRes) JSON(body any) Response {
|
func (b bodyRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(b)
|
return JSON(body).wrap(b)
|
||||||
}
|
}
|
||||||
@@ -82,3 +86,11 @@ func (b bodyRes) getCookies() []http.Cookie {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b bodyRes) getContentType() (contentType string, ok bool) {
|
||||||
|
if b.res != nil {
|
||||||
|
return b.res.getContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ func (c cookieRes) Redirect(code redirect.Code, to string) Response {
|
|||||||
return Redirect(code, to).wrap(c)
|
return Redirect(code, to).wrap(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) HTML(body []byte) Response {
|
||||||
|
return HTML(body).wrap(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (c cookieRes) JSON(body any) Response {
|
func (c cookieRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(c)
|
return JSON(body).wrap(c)
|
||||||
}
|
}
|
||||||
@@ -85,3 +89,11 @@ func (c cookieRes) getCookies() []http.Cookie {
|
|||||||
|
|
||||||
return []http.Cookie{c.cookie}
|
return []http.Cookie{c.cookie}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) getContentType() (contentType string, ok bool) {
|
||||||
|
if c.res != nil {
|
||||||
|
return c.res.getContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"ruben/inventory2/internal/site/redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
htmlRes struct {
|
||||||
|
body []byte
|
||||||
|
res Response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Response = htmlRes{}
|
||||||
|
|
||||||
|
func HTML(body []byte) Response {
|
||||||
|
return htmlRes{
|
||||||
|
body: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) String() string {
|
||||||
|
if h.res != nil {
|
||||||
|
return fmt.Sprintf(`{"body": %q, "nested": %s}`, string(h.body), h.res)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`{"body": %q}`, h.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) wrap(res Response) Response {
|
||||||
|
h.res = res
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) Status(code int) Response {
|
||||||
|
return Status(code).wrap(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) Redirect(code redirect.Code, to string) Response {
|
||||||
|
return Redirect(code, to).wrap(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) HTML(body []byte) Response {
|
||||||
|
h.body = body
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) JSON(body any) Response {
|
||||||
|
return JSON(body).wrap(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) Body(body io.ReadCloser) Response {
|
||||||
|
return Body(body).wrap(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) Cookie(ck http.Cookie) Response {
|
||||||
|
return Cookie(ck).wrap(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) getStatus() (int, bool) {
|
||||||
|
if h.res == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.res.getStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) getRedirect() (code redirect.Code, to string, ok bool) {
|
||||||
|
if h.res == nil {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.res.getRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
|
return io.NopCloser(bytes.NewBuffer(h.body)), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) getCookies() []http.Cookie {
|
||||||
|
if h.res != nil {
|
||||||
|
return h.res.getCookies()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) getContentType() (contentType string, ok bool) {
|
||||||
|
return "text/html", true
|
||||||
|
}
|
||||||
@@ -44,6 +44,10 @@ func (j jsonRes) Redirect(code redirect.Code, to string) Response {
|
|||||||
return Redirect(code, to).wrap(j)
|
return Redirect(code, to).wrap(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) HTML(body []byte) Response {
|
||||||
|
return HTML(body).wrap(j)
|
||||||
|
}
|
||||||
|
|
||||||
func (j jsonRes) JSON(body any) Response {
|
func (j jsonRes) JSON(body any) Response {
|
||||||
j.body = body
|
j.body = body
|
||||||
return j
|
return j
|
||||||
@@ -85,3 +89,7 @@ func (j jsonRes) getCookies() []http.Cookie {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) getContentType() (contentType string, ok bool) {
|
||||||
|
return "application/json", true
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ func (r redirectRes) Redirect(code redirect.Code, to string) Response {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) HTML(body []byte) Response {
|
||||||
|
return HTML(body).wrap(r)
|
||||||
|
}
|
||||||
|
|
||||||
func (r redirectRes) JSON(body any) Response {
|
func (r redirectRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(r)
|
return JSON(body).wrap(r)
|
||||||
}
|
}
|
||||||
@@ -122,3 +126,11 @@ func (r redirectRes) getCookies() []http.Cookie {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) getContentType() (contentType string, ok bool) {
|
||||||
|
if r.res != nil {
|
||||||
|
return r.res.getContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type (
|
|||||||
Status(int) Response
|
Status(int) Response
|
||||||
Redirect(code redirect.Code, to string) Response
|
Redirect(code redirect.Code, to string) Response
|
||||||
Body(io.ReadCloser) Response
|
Body(io.ReadCloser) Response
|
||||||
|
HTML([]byte) Response
|
||||||
JSON(any) Response
|
JSON(any) Response
|
||||||
Cookie(http.Cookie) Response
|
Cookie(http.Cookie) Response
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ type (
|
|||||||
getBody() (body io.ReadCloser, ok bool, err error)
|
getBody() (body io.ReadCloser, ok bool, err error)
|
||||||
getRedirect() (code redirect.Code, to string, ok bool)
|
getRedirect() (code redirect.Code, to string, ok bool)
|
||||||
getCookies() []http.Cookie
|
getCookies() []http.Cookie
|
||||||
|
getContentType() (contentType string, ok bool)
|
||||||
|
|
||||||
wrap(Response) Response
|
wrap(Response) Response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ func (s statusRes) Redirect(code redirect.Code, to string) Response {
|
|||||||
return Redirect(code, to).wrap(s)
|
return Redirect(code, to).wrap(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s statusRes) HTML(body []byte) Response {
|
||||||
|
return HTML(body).wrap(s)
|
||||||
|
}
|
||||||
|
|
||||||
func (s statusRes) JSON(body any) Response {
|
func (s statusRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(s)
|
return JSON(body).wrap(s)
|
||||||
}
|
}
|
||||||
@@ -83,3 +87,11 @@ func (s statusRes) getCookies() []http.Cookie {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s statusRes) getContentType() (contentType string, ok bool) {
|
||||||
|
if s.res != nil {
|
||||||
|
return s.res.getContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) {
|
|||||||
hdrs.Add("Set-Cookie", ck.String())
|
hdrs.Add("Set-Cookie", ck.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ct, ok := res.getContentType(); ok {
|
||||||
|
hdrs.Add("Content-Type", ct)
|
||||||
|
}
|
||||||
|
|
||||||
if code, to, ok := res.getRedirect(); ok {
|
if code, to, ok := res.getRedirect(); ok {
|
||||||
http.Redirect(w, r, to, code.Int())
|
http.Redirect(w, r, to, code.Int())
|
||||||
return
|
return
|
||||||
@@ -44,7 +48,7 @@ func Write(w http.ResponseWriter, r *http.Request, res Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func WriteError(w http.ResponseWriter, err error) {
|
func WriteError(w http.ResponseWriter, err error) {
|
||||||
var status int
|
status := http.StatusInternalServerError
|
||||||
|
|
||||||
if e, ok := GetError(err); ok {
|
if e, ok := GetError(err); ok {
|
||||||
if status, ok = e.GetStatus(); !ok {
|
if status, ok = e.GetStatus(); !ok {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--border-thin: 1px;
|
||||||
|
--border-medium: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
header,
|
header,
|
||||||
footer {
|
footer {
|
||||||
@@ -19,16 +24,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
border-width: 2px;
|
border-width: var(--border-medium);
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-thin {
|
.border-thin {
|
||||||
border-width: 1px;
|
border-width: var(--border-thin);
|
||||||
|
}
|
||||||
|
.border-medium {
|
||||||
|
border-width: var(--border-medium);
|
||||||
}
|
}
|
||||||
.border-button {
|
.border-button {
|
||||||
border-width: 2px;
|
border-width: var(--border-medium);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
@layer components {
|
||||||
|
table {
|
||||||
|
display: block; /* fixes scrolling when in a flexbox */
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
border-width: var(--border-thin) var(--border-thin) 0 var(--border-thin);
|
||||||
|
|
||||||
|
& > tr {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
|
||||||
|
& > :is(th, td) {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
background-color: var(--table-thead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& + tbody {
|
||||||
|
border-top-width: var(--border-thin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
border-color: var(--table-body-border) var(--table-border);
|
||||||
|
border-left-width: var(--border-thin);
|
||||||
|
border-right-width: var(--border-thin);
|
||||||
|
border-bottom-width: 0;
|
||||||
|
|
||||||
|
& > tr {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
|
||||||
|
& > :is(th, td) {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
background-color: var(--table-tbody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(+ tfoot) {
|
||||||
|
border-bottom-width: var(--border-thin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tfoot {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
border-width: 0 var(--border-thin) var(--border-thin) var(--border-thin);
|
||||||
|
|
||||||
|
& > tr {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
|
||||||
|
& > :is(th, td) {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
background-color: var(--table-tfoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-width: 0 0 var(--border-thin) 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border-width: 0 var(--border-thin) 0 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
--text-3xl: 1.875rem;
|
--text-3xl: 1.875rem;
|
||||||
--text-4xl: 2.25rem;
|
--text-4xl: 2.25rem;
|
||||||
--text-5xl: 3rem;
|
--text-5xl: 3rem;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
@@ -180,6 +181,9 @@
|
|||||||
.mt-\[3em\] {
|
.mt-\[3em\] {
|
||||||
margin-top: 3em;
|
margin-top: 3em;
|
||||||
}
|
}
|
||||||
|
.mb-\[0\.5em\] {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
.mb-\[1em\] {
|
.mb-\[1em\] {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
@@ -195,18 +199,27 @@
|
|||||||
.min-h-full {
|
.min-h-full {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.max-w-\[50\%\] {
|
.max-w-\[50\%\] {
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
.max-w-full {
|
.max-w-full {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.min-w-fit {
|
||||||
|
min-width: fit-content;
|
||||||
|
}
|
||||||
.grow {
|
.grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
.basis-full {
|
.basis-full {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
}
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -231,6 +244,9 @@
|
|||||||
.rounded-lg {
|
.rounded-lg {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
.rounded-sm {
|
||||||
|
border-radius: calc(var(--radius) - 4px);
|
||||||
|
}
|
||||||
.border-\[1px\] {
|
.border-\[1px\] {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@@ -244,6 +260,9 @@
|
|||||||
.bg-card {
|
.bg-card {
|
||||||
background-color: var(--card);
|
background-color: var(--card);
|
||||||
}
|
}
|
||||||
|
.p-\[0\.5em\] {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
.p-\[1em\] {
|
.p-\[1em\] {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
@@ -271,9 +290,30 @@
|
|||||||
--tw-font-weight: var(--font-weight-bold);
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
}
|
}
|
||||||
|
.font-semibold {
|
||||||
|
--tw-font-weight: var(--font-weight-semibold);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
.text-nowrap {
|
.text-nowrap {
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
.hover\:bg-accent {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:underline {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@layer base {
|
@layer base {
|
||||||
select {
|
select {
|
||||||
@@ -355,6 +395,12 @@
|
|||||||
--sidebar-accent-foreground: var(--base-800);
|
--sidebar-accent-foreground: var(--base-800);
|
||||||
--sidebar-border: var(--base-200);
|
--sidebar-border: var(--base-200);
|
||||||
--sidebar-ring: var(--primary-300);
|
--sidebar-ring: var(--primary-300);
|
||||||
|
--table-thead: var(--base-400);
|
||||||
|
--table-tbody: var(--base-200);
|
||||||
|
--table-tfoot: var(--base-400);
|
||||||
|
--table-border: var(--base-300);
|
||||||
|
--table-body-border: var(--base-500);
|
||||||
|
--table-inner-border: var(--base-300);
|
||||||
--display-color: var(--foreground);
|
--display-color: var(--foreground);
|
||||||
--text-color: var(--foreground);
|
--text-color: var(--foreground);
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@@ -389,11 +435,19 @@
|
|||||||
--sidebar-accent-foreground: var(--base-200);
|
--sidebar-accent-foreground: var(--base-200);
|
||||||
--sidebar-border: var(--base-800);
|
--sidebar-border: var(--base-800);
|
||||||
--sidebar-ring: var(--primary-300);
|
--sidebar-ring: var(--primary-300);
|
||||||
|
--table-thead: var(--base-600);
|
||||||
|
--table-tbody: var(--base-800);
|
||||||
|
--table-tfoot: var(--base-600);
|
||||||
|
--table-border: var(--base-700);
|
||||||
|
--table-body-border: var(--base-500);
|
||||||
|
--table-inner-border: var(--base-700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@layer theme {
|
||||||
* {
|
* {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--background: var(--base-950);
|
--background: var(--base-950);
|
||||||
--foreground: var(--base-200);
|
--foreground: var(--base-200);
|
||||||
@@ -477,6 +531,10 @@
|
|||||||
background-color: var(--sidebar-accent);
|
background-color: var(--sidebar-accent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
:root {
|
||||||
|
--border-thin: 1px;
|
||||||
|
--border-medium: 2px;
|
||||||
|
}
|
||||||
@layer base {
|
@layer base {
|
||||||
header, footer {
|
header, footer {
|
||||||
border-color: var(--sidebar-border);
|
border-color: var(--sidebar-border);
|
||||||
@@ -488,18 +546,83 @@
|
|||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
border-width: 2px;
|
border-width: var(--border-medium);
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
.border-thin {
|
.border-thin {
|
||||||
border-width: 1px;
|
border-width: var(--border-thin);
|
||||||
|
}
|
||||||
|
.border-medium {
|
||||||
|
border-width: var(--border-medium);
|
||||||
}
|
}
|
||||||
.border-button {
|
.border-button {
|
||||||
border-width: 2px;
|
border-width: var(--border-medium);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@layer components {
|
||||||
|
table {
|
||||||
|
display: block;
|
||||||
|
border-collapse: collapse;
|
||||||
|
thead {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
border-width: var(--border-thin) var(--border-thin) 0 var(--border-thin);
|
||||||
|
& > tr {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
& > :is(th, td) {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
background-color: var(--table-thead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& + tbody {
|
||||||
|
border-top-width: var(--border-thin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
border-color: var(--table-body-border) var(--table-border);
|
||||||
|
border-left-width: var(--border-thin);
|
||||||
|
border-right-width: var(--border-thin);
|
||||||
|
border-bottom-width: 0;
|
||||||
|
& > tr {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
& > :is(th, td) {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
background-color: var(--table-tbody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:has(+ tfoot) {
|
||||||
|
border-bottom-width: var(--border-thin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tfoot {
|
||||||
|
border-color: var(--table-border);
|
||||||
|
border-width: 0 var(--border-thin) var(--border-thin) var(--border-thin);
|
||||||
|
& > tr {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
& > :is(th, td) {
|
||||||
|
border-color: var(--table-body-border);
|
||||||
|
background-color: var(--table-tfoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
border-width: 0 0 var(--border-thin) 0;
|
||||||
|
&:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border-width: 0 var(--border-thin) 0 0;
|
||||||
|
&:last-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@property --tw-border-style {
|
@property --tw-border-style {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
|
|||||||
@@ -14,3 +14,4 @@
|
|||||||
@import "./interactivity.css";
|
@import "./interactivity.css";
|
||||||
@import "./svg.css";
|
@import "./svg.css";
|
||||||
@import "./accessibility.css";
|
@import "./accessibility.css";
|
||||||
|
@import "./components.css";
|
||||||
|
|||||||
@@ -80,6 +80,13 @@
|
|||||||
--sidebar-border: var(--base-200);
|
--sidebar-border: var(--base-200);
|
||||||
--sidebar-ring: var(--primary-300);
|
--sidebar-ring: var(--primary-300);
|
||||||
|
|
||||||
|
--table-thead: var(--base-400);
|
||||||
|
--table-tbody: var(--base-200);
|
||||||
|
--table-tfoot: var(--base-400);
|
||||||
|
--table-border: var(--base-300);
|
||||||
|
--table-body-border: var(--base-500);
|
||||||
|
--table-inner-border: var(--base-300);
|
||||||
|
|
||||||
--display-color: var(--foreground);
|
--display-color: var(--foreground);
|
||||||
--text-color: var(--foreground);
|
--text-color: var(--foreground);
|
||||||
|
|
||||||
@@ -115,12 +122,21 @@
|
|||||||
--sidebar-accent-foreground: var(--base-200);
|
--sidebar-accent-foreground: var(--base-200);
|
||||||
--sidebar-border: var(--base-800);
|
--sidebar-border: var(--base-800);
|
||||||
--sidebar-ring: var(--primary-300);
|
--sidebar-ring: var(--primary-300);
|
||||||
|
|
||||||
|
--table-thead: var(--base-600);
|
||||||
|
--table-tbody: var(--base-800);
|
||||||
|
--table-tfoot: var(--base-600);
|
||||||
|
--table-border: var(--base-700);
|
||||||
|
--table-body-border: var(--base-500);
|
||||||
|
--table-inner-border: var(--base-700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer theme {
|
||||||
* {
|
* {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: var(--base-950);
|
--background: var(--base-950);
|
||||||
|
|||||||
+30
-16
@@ -15,11 +15,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GET /
|
// GET /
|
||||||
|
// compiles the page template or component template matching the url
|
||||||
func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
||||||
ctx := r.Context()
|
name, args := s.getTemplateNameAndArgs(r, "internal/site/templates/component_bodies")
|
||||||
name, pathParams := getPageTemplateNameForURL(r.URL)
|
|
||||||
|
|
||||||
templateArgs := []any{
|
b, err := s.templater.ExecuteComponentBody(name, args...)
|
||||||
|
if err == nil {
|
||||||
|
return response.HTML(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isFileNotFoundError(err) {
|
||||||
|
return s.handleTemplateError(err, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, args = s.getTemplateNameAndArgs(r, "internal/site/templates/page_bodies")
|
||||||
|
|
||||||
|
if b, err = s.templater.ExecutePage(name, args...); err == nil {
|
||||||
|
return response.HTML(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.handleTemplateError(err, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getTemplateNameAndArgs(r *http.Request, templateDir string) (name string, args []any) {
|
||||||
|
ctx := r.Context()
|
||||||
|
name, pathParams := getTemplateNameForURL(r.URL, templateDir)
|
||||||
|
|
||||||
|
return name, []any{
|
||||||
"Request",
|
"Request",
|
||||||
r,
|
r,
|
||||||
// add services and data here
|
// add services and data here
|
||||||
@@ -47,22 +69,15 @@ func (s *Server) serveTemplates(r *http.Request) (response.Response, error) {
|
|||||||
"Auth",
|
"Auth",
|
||||||
newTemplateAuthenticator(r),
|
newTemplateAuthenticator(r),
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := s.templater.ExecutePage(name, templateArgs...)
|
|
||||||
if err != nil {
|
|
||||||
return s.handleTemplateError(err, templateArgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Body(io.NopCloser(bytes.NewBuffer(b))), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: clean this up...
|
// TODO: clean this up...
|
||||||
// TODO: somehow tell what the path params are and pass them up.
|
// TODO: somehow tell what the path params are and pass them up.
|
||||||
// - then consider pushing this functionality into the template library.
|
// - then consider pushing this functionality into the template library.
|
||||||
//
|
//
|
||||||
// getPageTemplateNameForURL eliminate any trailing .html or /, and checks for any
|
// getComponentTemplateNameForURL eliminate any trailing .html or /, and checks for any
|
||||||
// file with path parameters in the name, eg '{abc}.html.tmpl', prefering exact filename matches.
|
// file with path parameters in the name, eg '{abc}.html.tmpl', prefering exact filename matches.
|
||||||
func getPageTemplateNameForURL(u *url.URL) (name string, params map[string]string) {
|
func getTemplateNameForURL(u *url.URL, templateDir string) (name string, params map[string]string) {
|
||||||
fp := strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(u.Path, ".html"), "/"), "/")
|
fp := strings.TrimPrefix(strings.TrimSuffix(strings.TrimSuffix(u.Path, ".html"), "/"), "/")
|
||||||
if fp == "" {
|
if fp == "" {
|
||||||
// "/" maps to "/index"
|
// "/" maps to "/index"
|
||||||
@@ -72,17 +87,16 @@ func getPageTemplateNameForURL(u *url.URL) (name string, params map[string]strin
|
|||||||
fpParts := strings.Split(fp, "/")
|
fpParts := strings.Split(fp, "/")
|
||||||
res := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(fpParts)
|
res := getMatchingGlobPatternsCapturingFilepathIncludingParametrizedFilepaths(fpParts)
|
||||||
for _, combs := range res {
|
for _, combs := range res {
|
||||||
const pageBodiesPrefix = "internal/site/templates/page_bodies"
|
pattern := path.Join(templateDir, path.Join(combs...)) + ".html.tmpl"
|
||||||
pattern := path.Join(pageBodiesPrefix, path.Join(combs...)) + ".html.tmpl"
|
|
||||||
|
|
||||||
matches, _ := filepath.Glob(pattern)
|
matches, _ := filepath.Glob(pattern)
|
||||||
if len(matches) == 0 {
|
if len(matches) == 0 {
|
||||||
pattern := path.Join(pageBodiesPrefix, path.Join(combs...), "index") + ".html.tmpl"
|
pattern := path.Join(templateDir, path.Join(combs...), "index") + ".html.tmpl"
|
||||||
matches, _ = filepath.Glob(pattern)
|
matches, _ = filepath.Glob(pattern)
|
||||||
}
|
}
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
match := matches[0]
|
match := matches[0]
|
||||||
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), pageBodiesPrefix+"/")
|
name = strings.TrimPrefix(strings.TrimSuffix(match, ".html.tmpl"), templateDir+"/")
|
||||||
|
|
||||||
patternParts := strings.Split(name, "/")
|
patternParts := strings.Split(name, "/")
|
||||||
params = make(map[string]string)
|
params = make(map[string]string)
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
{{/* .Identity.Account.AccountID | .PathParams.acctID | .AccountID */}}
|
||||||
|
|
||||||
|
{{- $dot := or .dot . }}
|
||||||
|
{{- .Auth.ByMatchingAccountID 2 }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{{- $acctID := 0 }}
|
||||||
|
{{- if $dot.Identity }}
|
||||||
|
{{- $acctID = $dot.Identity.Account.AccountID }}
|
||||||
|
{{- else if .PathParams }}
|
||||||
|
{{- $acctID = $dot.PathParams.acctID | parseInt64 }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $acctID = $dot.AccountID }}
|
||||||
|
{{- end }}
|
||||||
|
{{- $stores := $dot.Accounts.GetShops $acctID -}}
|
||||||
|
|
||||||
|
|
||||||
|
<table id="inventory-table-2" class="max-w-full overflow-x-scroll">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Shop
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Listing
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
SKU
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{/* TODO: list existing row */}}
|
||||||
|
{{- componentBody "accounts/{acctID}/inventory/sync-table-v2/new-row" "dot" $dot }}
|
||||||
|
{{/* TODO: save the saved rows */}}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Shop
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Listing
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
SKU
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
{{/* .Identity.Account.AccountID, .Accounts */}}
|
||||||
|
|
||||||
|
{{- $dot := or .dot . -}}
|
||||||
|
|
||||||
|
|
||||||
|
{{- $acctID := $dot.Identity.Account.AccountID }}
|
||||||
|
{{- $shops := $dot.Accounts.GetShops $acctID -}}
|
||||||
|
|
||||||
|
|
||||||
|
<tr _="
|
||||||
|
on setListing(listing)
|
||||||
|
set the innerHTML of (the last <td[data-id=sku]/> in me) to listing.sku
|
||||||
|
set the innerHTML of (the last <td[data-id=description]/> in me) to listing.description
|
||||||
|
-- TODO: only remove if there isn't already a 'new row'
|
||||||
|
remove @disabled from (the <button/> in last <td[data-id=save]/> in me)
|
||||||
|
on resetListing
|
||||||
|
set the innerHTML of (the last <td[data-id=sku]/> in me) to '-'
|
||||||
|
set the innerHTML of (the last <td[data-id=description]/> in me) to '-'
|
||||||
|
add @disabled to (the <button/> in last <td[data-id=save]/> in me)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
class="min-w-fit cursor-pointer"
|
||||||
|
hx-get="/accounts/{{$acctID}}/platforms/{platform}/shops/{shopID}/listing-select"
|
||||||
|
hx-vals='js:{
|
||||||
|
platform: event.target.value.replace(/-[^ ]*/, ""),
|
||||||
|
shopID: event.target.value.replace(/[^ ]*-/, "")
|
||||||
|
}'
|
||||||
|
hx-target="next td"
|
||||||
|
_="
|
||||||
|
on change
|
||||||
|
log event.target.value
|
||||||
|
send resetListing() to closest <tr/>
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<option disabled selected>- Shops -</option>
|
||||||
|
{{- range $shop := $shops }}
|
||||||
|
<option value="{{$shop.Platform}}-{{$shop.ShopID}}">
|
||||||
|
{{ $shop.Name }} - {{$shop.Platform}} - {{$shop.ShopID}}
|
||||||
|
</option>
|
||||||
|
{{- end }}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
-
|
||||||
|
</td>
|
||||||
|
<td data-id="sku">
|
||||||
|
-
|
||||||
|
</td>
|
||||||
|
<td data-id="description">
|
||||||
|
-
|
||||||
|
</td>
|
||||||
|
<td data-id="save">
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
hx-get="/accounts/{{$acctID}}/inventory/sync-table-v2/new-row"
|
||||||
|
hx-target="closest tr"
|
||||||
|
hx-swap="afterend"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
{{- $dot := or .dot . }}
|
||||||
|
|
||||||
|
{{- $dot.Auth.ByMatchingAccountID 2 }}
|
||||||
|
|
||||||
|
{{- $acctID := $dot.Identity.Account.AccountID }}
|
||||||
|
{{- $stores := $dot.Accounts.GetShops $acctID -}}
|
||||||
|
|
||||||
|
|
||||||
|
<table id="inventory-table" class="max-w-full overflow-x-scroll">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
{{- range $store := $stores }}
|
||||||
|
<th class="text-nowrap">
|
||||||
|
{{ $store.Platform }}: {{ $store.Name }}
|
||||||
|
</th>
|
||||||
|
<th class="text-nowrap">
|
||||||
|
Count
|
||||||
|
</th>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
<th>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{- componentBody "accounts/{acctID}/inventory/sync-table/new-row" "dot" $dot }}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
{{- range $store := $stores }}
|
||||||
|
<th class="text-nowrap">
|
||||||
|
{{ $store.Platform }}: {{ $store.Name }}
|
||||||
|
</th>
|
||||||
|
<th class="text-nowrap">
|
||||||
|
Count
|
||||||
|
</th>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
<th>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
{{- $dot := or .dot .}}
|
||||||
|
|
||||||
|
{{- $dot.Auth.ByMatchingAccountID 2 }}
|
||||||
|
|
||||||
|
{{- $acctID := $dot.Identity.Account.AccountID }}
|
||||||
|
{{- $shops := $dot.Accounts.GetShops $acctID -}}
|
||||||
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
{{- range $shop := $shops }}
|
||||||
|
{{- $listings := $dot.Accounts.GetListingsForShop $acctID $shop.ShopID }}
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
class="min-w-fit cursor-pointer"
|
||||||
|
_="
|
||||||
|
on change
|
||||||
|
set dataset to the dataset of the first of my selectedOptions
|
||||||
|
|
||||||
|
set listingShowcase to the <#listing-showcase/>
|
||||||
|
if <#listing-showcase/> then
|
||||||
|
send displayListing(
|
||||||
|
id: dataset.id,
|
||||||
|
name: dataset.name,
|
||||||
|
description: dataset.description,
|
||||||
|
count: dataset.count
|
||||||
|
) to listingShowcase
|
||||||
|
end
|
||||||
|
|
||||||
|
set the innerHTML of the next <td/> to the dataset.count
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<option disabled selected>- Listings -</option>
|
||||||
|
{{- range $listing := $listings }}
|
||||||
|
<option
|
||||||
|
value="{{$listing.ListingID}}"
|
||||||
|
data-id="{{$listing.ListingID}}"
|
||||||
|
data-name="{{$listing.Name}}"
|
||||||
|
data-description="{{$listing.Description}}"
|
||||||
|
data-count="{{$listing.Count}}"
|
||||||
|
>
|
||||||
|
{{ $listing.Name }}
|
||||||
|
</option>
|
||||||
|
{{- end }}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
{{- end }}
|
||||||
|
<td>
|
||||||
|
<button class="cursor-pointer underline w-full">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
<table class="max-w-full"
|
||||||
|
id="listing-showcase"
|
||||||
|
_="
|
||||||
|
on displayListing(id, name, description, count)
|
||||||
|
set innerHTML of <#listing-showcase-id/> to id
|
||||||
|
set innerHTML of <#listing-showcase-name/> to name
|
||||||
|
set innerHTML of <#listing-showcase-description/> to description
|
||||||
|
set innerHTML of <#listing-showcase-count/> to count
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<th>
|
||||||
|
ID
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Count
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<th id="listing-showcase-id">
|
||||||
|
-
|
||||||
|
</th>
|
||||||
|
<th id="listing-showcase-name">
|
||||||
|
-
|
||||||
|
</th>
|
||||||
|
<th id="listing-showcase-description">
|
||||||
|
-
|
||||||
|
</th>
|
||||||
|
<th id="listing-showcase-count">
|
||||||
|
-
|
||||||
|
</th>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{{/* .ShopID: default to path param "shopID" */}}
|
||||||
|
|
||||||
|
{{- $shopID := or (and .PathParams .PathParams.shopID) .ShopID }}
|
||||||
|
|
||||||
|
{{ $acctID := .Identity.Account.AccountID }}
|
||||||
|
|
||||||
|
<select
|
||||||
|
class="min-w-fit cursor-pointer"
|
||||||
|
_="
|
||||||
|
on input
|
||||||
|
set dataset to (my selectedOptions)[0].dataset
|
||||||
|
send setListing(listing: dataset) to the closest <tr/>
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{- $listings := .Accounts.GetListingsForShop $acctID $shopID }}
|
||||||
|
{{- $dot := . }}
|
||||||
|
<option disabled selected>- Listings -</option>
|
||||||
|
{{- range $listing := $listings }}
|
||||||
|
<option
|
||||||
|
value="{{$listing.SKU}}"
|
||||||
|
data-name="{{ $listing.Name }}"
|
||||||
|
data-sku="{{ $listing.SKU }}"
|
||||||
|
data-description="{{ $listing.Description }}"
|
||||||
|
data-count="{{ $listing.Count }}"
|
||||||
|
>
|
||||||
|
{{ $listing.Name }}
|
||||||
|
</option>
|
||||||
|
{{- end }}
|
||||||
|
</select>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
{{- $acct := .dot.Identity.Account }}
|
{{- $acct := .dot.Identity.Account }}
|
||||||
{{- $acctID := 0 }}
|
{{- $acctID := 0 }}
|
||||||
{{- if $acct }}
|
{{- if $acct }}
|
||||||
{{- $acctID = $acct.ID }}
|
{{- $acctID = $acct.AccountID }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- if not $userID }}
|
{{- if not $userID }}
|
||||||
|
|||||||
@@ -11,12 +11,13 @@
|
|||||||
|
|
||||||
<script src="/scripts/htmx.min.js.gz"></script>
|
<script src="/scripts/htmx.min.js.gz"></script>
|
||||||
<script src="/scripts/_hyperscript.min.js.gz"></script>
|
<script src="/scripts/_hyperscript.min.js.gz"></script>
|
||||||
|
<script src="https://unpkg.com/htmx-ext-path-params@2.0.0/path-params.js"></script>
|
||||||
|
|
||||||
{{/* This is where the page_head template will be inserted into the page */}}
|
{{/* This is where the page_head template will be inserted into the page */}}
|
||||||
{{- block "head" . }}{{ end }}
|
{{- block "head" . }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="basis-full grow bg-background flex flex-col items-stretch">
|
<body class="basis-full grow bg-background flex flex-col items-stretch" hx-ext="path-params">
|
||||||
<header>
|
<header>
|
||||||
<nav class="flex flex-col items-center text-xl overflow-x-scroll overflow-y-hidden ">
|
<nav class="flex flex-col items-center text-xl overflow-x-scroll overflow-y-hidden ">
|
||||||
{{- $path := or .Request.URL.Path "/" }}
|
{{- $path := or .Request.URL.Path "/" }}
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
{{- $acct := .Identity.Account }}
|
{{- $acct := .Identity.Account }}
|
||||||
{{- $acctID := 0 }}
|
{{- $acctID := 0 }}
|
||||||
{{- if $acct }}
|
{{- if $acct }}
|
||||||
{{- $acctID = $acct.ID }}
|
{{- $acctID = $acct.AccountID }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{- if not $userID }}
|
{{- if not $userID }}
|
||||||
@@ -53,14 +54,14 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="pt-[1em] pb-[1em]">
|
<li class="pt-[1em] pb-[1em]">
|
||||||
<a href="/accounts/{{$acctID}}/reports" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/reports" $acctID) }}selected{{ end }}">
|
<a href="/accounts/{{$acctID}}/inventory" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/inventory" $acctID) }}selected{{ end }}">
|
||||||
Reports
|
Inventory
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="pt-[1em] pb-[1em]">
|
<li class="pt-[1em] pb-[1em]">
|
||||||
<a href="/accounts/{{$acctID}}/inventory" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/inventory" $acctID) }}selected{{ end }}">
|
<a href="/accounts/{{$acctID}}/reports" class="p-[1em] font-bold {{ if eq $path (printf "/accounts/%d/reports" $acctID) }}selected{{ end }}">
|
||||||
Inventory
|
Reports
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,15 @@
|
|||||||
|
|
||||||
<h1>Account: {{ $acctID.Email }}</h1>
|
<h1>Account: {{ $acctID.Email }}</h1>
|
||||||
|
|
||||||
{{- $etsyUser := .Etsy.GetUserPointerByAccountID $acctID.ID }}
|
{{- $etsyUser := .Etsy.GetUserPointerByAccountID $acctID.AccountID }}
|
||||||
{{- if $etsyUser }}
|
{{- if $etsyUser }}
|
||||||
<h3>Etsy User: {{ $etsyUser.UserID }}; Shop ID: {{ $etsyUser.ShopID }}</h3>
|
<h3>Etsy User: {{ $etsyUser.UserID }}; Shop ID: {{ $etsyUser.ShopID }}</h3>
|
||||||
{{- else }}
|
{{- else }}
|
||||||
<h3>
|
<h3>
|
||||||
{{/* TODO: create this link dynamically, not EVERYTIME THE PAGE IS LOADED */}}
|
{{/* TODO: create this link dynamically, not EVERYTIME THE PAGE IS LOADED */}}
|
||||||
<a href="{{ printf "/webhooks/etsy/%d/new-account-link" $acctID.ID }}">
|
<a href="{{ printf "/webhooks/etsy/%d/new-account-link" $acctID.AccountID }}">
|
||||||
Link Your Etsy Store!
|
Link Your Etsy Store!
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
<h2><a href="/accounts/{{$acctID.ID}}/reports">View Reports</a></h2>
|
<h2><a href="/accounts/{{$acctID.AccountID}}/reports">View Reports</a></h2>
|
||||||
|
|||||||
@@ -2,4 +2,38 @@
|
|||||||
|
|
||||||
{{- define "title" }} Inventory++ {{ end }}
|
{{- define "title" }} Inventory++ {{ end }}
|
||||||
|
|
||||||
<h1>Inventory management page: WIP</h1>
|
<h1 class="text-center mb-[0.5em]">Inventory</h1>
|
||||||
|
|
||||||
|
<h2 class="text-center mb-[0.5em]">Sync Stores</h2>
|
||||||
|
|
||||||
|
{{ $acctID := .Identity.Account.AccountID }}
|
||||||
|
{{ $stores := .Accounts.GetShops $acctID }}
|
||||||
|
|
||||||
|
<section class="w-full flex flex-col items-center">
|
||||||
|
<h3>
|
||||||
|
Draft 2
|
||||||
|
</h3>
|
||||||
|
{{ componentBody "accounts/{acctID}/inventory/sync-table-v2" "dot" . }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="w-full flex flex-col items-center">
|
||||||
|
<h3>
|
||||||
|
Draft 1
|
||||||
|
</h3>
|
||||||
|
{{ componentBody "accounts/{acctID}/inventory/sync-table" "dot" . }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="flex justify-center mt-[1em]">
|
||||||
|
{{ componentBody "accounts/{acctID}/inventory/sync-table/showcase-table" "dot" . }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="w-full flex justify-center mt-[1em]">
|
||||||
|
<button
|
||||||
|
class="border-thin bg-card rounded-sm p-[0.5em] font-semibold cursor-pointer hover:underline hover:bg-accent"
|
||||||
|
hx-get="/accounts/{{$acctID}}/inventory/sync-table/new-row"
|
||||||
|
hx-target="#inventory-table > tbody"
|
||||||
|
hx-swap="beforeend"
|
||||||
|
>
|
||||||
|
Add Row
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{- if and .Identity.User.UserID (not .Identity.Account) }}
|
{{- if and .Identity.User.UserID (not .Identity.Account) }}
|
||||||
|
{{/* TODO: make account creation automatic as part of the user creation process */}}
|
||||||
<h2><a href="/account-creation">New Account</a></h2>
|
<h2><a href="/account-creation">New Account</a></h2>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user