Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eaac0d5
progress on middleware
manueltorres0 Jan 21, 2026
a48c7e7
middleware done + secret key
manueltorres0 Jan 21, 2026
bcad7d5
server changes
manueltorres0 Jan 21, 2026
5bfc0a8
details
manueltorres0 Jan 22, 2026
f6e2e4e
progress
manueltorres0 Jan 23, 2026
1182578
setting up webhook endpoint
manueltorres0 Jan 23, 2026
244e700
webhook endpoint
manueltorres0 Jan 23, 2026
36ba6e6
missing tests
manueltorres0 Jan 23, 2026
c19b865
tests for hook and middleware
manueltorres0 Jan 24, 2026
7064708
temporary user endpoint fix
manueltorres0 Jan 24, 2026
6e475c4
lint fix
manueltorres0 Jan 24, 2026
3c690b0
user repo insert fix
manueltorres0 Jan 24, 2026
30a3c94
unsure
manueltorres0 Jan 29, 2026
c9f9f79
merged
manueltorres0 Jan 29, 2026
c56b84d
comments
manueltorres0 Jan 29, 2026
2ec3cf5
comments
manueltorres0 Jan 29, 2026
89de6ec
small fixes
manueltorres0 Jan 29, 2026
6d312ef
weird dependency thing
manueltorres0 Jan 29, 2026
be12eac
ts dependency
manueltorres0 Jan 29, 2026
7ccb3ff
maybe
manueltorres0 Jan 29, 2026
d7b05e2
dependencies change
manueltorres0 Jan 29, 2026
c81f728
workflow yaml fix
manueltorres0 Jan 31, 2026
4d692f2
synced package.json
manueltorres0 Jan 31, 2026
83c470c
mobile fix
manueltorres0 Jan 31, 2026
d19498b
merged main
manueltorres0 Jan 31, 2026
dc00cfb
backend comments and set clients to main
manueltorres0 Feb 3, 2026
057ec8d
small change to main
manueltorres0 Feb 3, 2026
c8be1e2
merged main
manueltorres0 Feb 3, 2026
6bc2a13
Merge branch 'main' into feat/clerk-middleware-user-link
manueltorres0 Feb 3, 2026
2efba7f
fixed go.mod
manueltorres0 Feb 3, 2026
da3332f
Merge branch 'main' into feat/clerk-middleware-user-link
manueltorres0 Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion backend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ DB_NAME=""
PORT="8080"
APP_LOG_LEVEL="info"

DEV_CLERK_SECRET_KEY="secret"
DEV_CLERK_WEBHOOK_SIGNATURE="secret"
ENV="dev/prod"

NGROK_DOMAIN="your own stable domain"
LLM_SERVER_ADDRESS="http://127.0.0.1:11434"
LLM_MODEL="qwen2.5:7b-instruct"
LLM_TIMEOUT="60"
LLM_MAX_OUTPUT_TOKENS="1024"
LLM_TEMPERATURE="0.2"
LLM_TEMPERATURE="0.2"
3 changes: 3 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ run: llm-start db-start
@$(LOAD_ENV) \
go run $(CMD_PATH)

ngrok:
@$(LOAD_ENV) ngrok http --domain=$$NGROK_DOMAIN $(PORT)

dev: llm-start db-start clean build
@$(LOAD_ENV) \
./bin/$(APP_NAME)
Expand Down
7 changes: 6 additions & 1 deletion backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"os/signal"
"syscall"
"fmt"

"github.com/generate/selfserve/config"
_ "github.com/generate/selfserve/docs"
Expand Down Expand Up @@ -42,7 +43,11 @@ func main() {
log.Fatal("failed to initialize app:", err)
}

defer app.Repo.Close()
defer func() {
if err := app.Repo.Close(); err != nil {
panic(fmt.Sprintf("failed to close repo: %v", err))
}
}()

go func() {
if err := app.Server.Listen(":" + cfg.Application.Port); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/generate/selfserve
go 1.24.1

require (
github.com/clerk/clerk-sdk-go/v2 v2.5.1
github.com/firebase/genkit/go v1.4.0
github.com/gofiber/adaptor/v2 v2.2.1
github.com/sethvargo/go-envconfig v1.3.0
Expand All @@ -18,6 +19,7 @@ require (
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
Expand Down Expand Up @@ -52,6 +54,7 @@ require (
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
Expand All @@ -70,6 +73,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/svix/svix-webhooks v1.84.1
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
golang.org/x/sys v0.40.0 // indirect
Expand Down
23 changes: 23 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/clerk/clerk-sdk-go/v2 v2.5.1 h1:RsakGNW6ie83b9KIRtKzqDXBJ//cURy9SJUbGhrsIKg=
github.com/clerk/clerk-sdk-go/v2 v2.5.1/go.mod h1:ncFmsPwmD5WpGCNW5bJve862j/HQfpkzsshXYV/quJ8=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
Expand All @@ -15,6 +17,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/firebase/genkit/go v1.4.0 h1:CP1hNWk7z0hosyY53zMH6MFKFO1fMLtj58jGPllQo6I=
github.com/firebase/genkit/go v1.4.0/go.mod h1:HX6m7QOaGc3MDNr/DrpQZrzPLzxeuLxrkTvfFtCYlGw=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand Down Expand Up @@ -57,6 +61,7 @@ github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254 h1:okN800+zMJOGHLJCgry+OGzhhtH6YrjQh1rluHmOacE=
github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254/go.mod h1:k8cjJAQWc//ac/bMnzItyOFbfT01tgRTZGgxELCuxEQ=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand All @@ -71,6 +76,8 @@ github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
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/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down Expand Up @@ -99,6 +106,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/svix/svix-webhooks v1.84.1 h1:N8L4TZAxpFLi+dT4T7Zweorwzqx1lYgGUhedbF3Nb6M=
github.com/svix/svix-webhooks v1.84.1/go.mod h1:BRbQWn/xdv6zSGULojHza0Yx+hDf+xUJ4s09t3HqJpI=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
Expand Down Expand Up @@ -139,17 +148,24 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand All @@ -159,20 +175,27 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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=
Expand Down
88 changes: 88 additions & 0 deletions backend/internal/handler/clerk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package handler

import (
"net/http"
"os"
"strings"

"github.com/generate/selfserve/internal/errs"
"github.com/generate/selfserve/internal/models"
storage "github.com/generate/selfserve/internal/service/storage/postgres"
"github.com/gofiber/fiber/v2"
svix "github.com/svix/svix-webhooks/go"
)

type ClerkWebHookHandler struct {
UsersRepository storage.UsersRepository
WebhookVerifier WebhookVerifier
}

type WebhookVerifier interface {
Verify(payload []byte, headers http.Header) error
}

func NewWebhookVerifier() (WebhookVerifier, error) {
return svix.NewWebhook(os.Getenv("DEV_CLERK_WEBHOOK_SIGNATURE"))
}

func NewClerkWebHookHandler(userRepo storage.UsersRepository, WebhookVerifier WebhookVerifier) *ClerkWebHookHandler {
return &ClerkWebHookHandler{UsersRepository: userRepo, WebhookVerifier: WebhookVerifier}
}

func (h *ClerkWebHookHandler) CreateUser(c *fiber.Ctx) error {
headers := http.Header{}
headers.Set("svix-id", c.Get("svix-id"))
headers.Set("svix-timestamp", c.Get("svix-timestamp"))
headers.Set("svix-signature", c.Get("svix-signature"))

err := h.WebhookVerifier.Verify(c.Body(), headers)
if err != nil {
return errs.Unauthorized()
}

var CreateUserRequest models.CreateUserWebhook
if err := c.BodyParser(&CreateUserRequest); err != nil {
return errs.InvalidJSON()
}

if err := validateCreateUserClerk(&CreateUserRequest); err != nil {
return err
}

res, err := h.UsersRepository.InsertUser(c.Context(), reformatUserData(CreateUserRequest))
if err != nil {
return errs.InternalServerError()
}

return c.JSON(res)
}

func validateCreateUserClerk(user *models.CreateUserWebhook) error {
errors := make(map[string]string)

if strings.TrimSpace(user.Data.ID) == "" {
errors["id"] = "must not be an empty string"
}

if strings.TrimSpace(user.Data.FirstName) == "" {
errors["first_name"] = "must not be an empty string"
}

if strings.TrimSpace(user.Data.LastName) == "" {
errors["last_name"] = "must not be an empty string"
}

return AggregateErrors(errors)
}

func reformatUserData(CreateUserRequest models.CreateUserWebhook) *models.CreateUser {
result := &models.CreateUser{
FirstName: CreateUserRequest.Data.FirstName,
LastName: CreateUserRequest.Data.LastName,
ClerkID: CreateUserRequest.Data.ID,
}
if CreateUserRequest.Data.HasImage {
result.ProfilePicture = CreateUserRequest.Data.ImageUrl
}
return result
}
26 changes: 6 additions & 20 deletions backend/internal/handler/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"log/slog"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -93,30 +92,17 @@ func validateCreateUser(user *models.CreateUser) error {
errors["last_name"] = "must not be an empty string"
}

if strings.TrimSpace(user.Role) == "" {
errors["role"] = "must not be an empty string"
}

if user.Timezone != nil {
if _, err := time.LoadLocation(*user.Timezone); err != nil {
_, err := time.LoadLocation(*user.Timezone)
if err != nil || !strings.Contains(*user.Timezone, "/") {
errors["timezone"] = "invalid IANA timezone"
}
}

// Aggregates errors deterministically
if len(errors) > 0 {
var keys []string
for k := range errors {
keys = append(keys, k)
}
sort.Strings(keys)

var parts []string
for _, k := range keys {
parts = append(parts, k+": "+errors[k])
}
return errs.BadRequest(strings.Join(parts, ", "))
if strings.TrimSpace(user.ClerkID) == "" {
errors["clerk_id"] = "must not be an empty string"
}

return nil
// Aggregates errors deterministically
return AggregateErrors(errors)
}
41 changes: 37 additions & 4 deletions backend/internal/handler/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ func TestUsersHandler_GetUserByID(t *testing.T) {

mock := &mockUsersRepository{
findUserByIdFunc: func(ctx context.Context, id string) (*models.User, error) {
role := "admin"
return &models.User{
CreateUser: models.CreateUser{
FirstName: "John",
LastName: "Doe",
Role: "admin",
Role: &role,
},
ID: "550e8400-e29b-41d4-a716-446655440000",
}, nil
Expand Down Expand Up @@ -158,7 +159,8 @@ func TestUsersHandler_CreateUser(t *testing.T) {
validBody := `{
"first_name": "John",
"last_name": "Doe",
"role": "Receptionist"
"role": "Receptionist",
"clerk_id": "user_123"
}`

t.Run("returns 200 on valid user creation", func(t *testing.T) {
Expand Down Expand Up @@ -217,7 +219,8 @@ func TestUsersHandler_CreateUser(t *testing.T) {
"role": "Manager",
"employee_id": "EMP-67",
"department": "Front Desk",
"timezone": "America/New_York"
"timezone": "America/New_York",
"clerk_id": "user_123"
}`

req := httptest.NewRequest("POST", "/users", bytes.NewBufferString(bodyWithOptionals))
Expand Down Expand Up @@ -271,7 +274,7 @@ func TestUsersHandler_CreateUser(t *testing.T) {
body, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(body), "first_name")
assert.Contains(t, string(body), "last_name")
assert.Contains(t, string(body), "role")
assert.Contains(t, string(body), "clerk_id")
})

t.Run("returns 400 on invalid timezone", func(t *testing.T) {
Expand All @@ -287,6 +290,7 @@ func TestUsersHandler_CreateUser(t *testing.T) {
"first_name": "John",
"last_name": "Doe",
"role": "Receptionist",
"clerk_id": "user_123",
"timezone": "Invalid/Not_A_Timezone"
}`

Expand Down Expand Up @@ -323,4 +327,33 @@ func TestUsersHandler_CreateUser(t *testing.T) {

assert.Equal(t, 500, resp.StatusCode)
})

t.Run("returns_400_when_clerk_id_is_missing", func(t *testing.T) {
body := `{
"first_name": "John",
"last_name": "Doe",
"role": "Receptionist"
}`

mock := &mockUsersRepository{
insertUserFunc: func(ctx context.Context, user *models.CreateUser) (*models.User, error) {
return nil, nil
},
}

app := fiber.New(fiber.Config{ErrorHandler: errs.ErrorHandler})
h := NewUsersHandler(mock)
app.Post("/users", h.CreateUser)

req := httptest.NewRequest("POST", "/users", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")

resp, err := app.Test(req)
require.NoError(t, err)

assert.Equal(t, 400, resp.StatusCode)
respBody, _ := io.ReadAll(resp.Body)
assert.Contains(t, string(respBody), "clerk_id")

})
}
Loading
Loading