From 09c1a48d185ea7a6d8fefe905a40654e6b5b943e Mon Sep 17 00:00:00 2001 From: Zeke Abshire Date: Sun, 10 Dec 2023 21:58:30 -0600 Subject: [PATCH] feat: integrated Clerk webhooks --- .env.example | 21 +- README.md | 10 + package-lock.json | 281 ++++++++++++++++++ package.json | 2 + .../api/webhooks/sync-user-id/createUser.ts | 10 + src/app/api/webhooks/sync-user-id/route.ts | 101 +++++++ src/env.js | 2 + src/middleware.ts | 2 +- src/server/db/schema.ts | 2 +- 9 files changed, 417 insertions(+), 14 deletions(-) create mode 100644 src/app/api/webhooks/sync-user-id/createUser.ts create mode 100644 src/app/api/webhooks/sync-user-id/route.ts diff --git a/.env.example b/.env.example index e00c4fc..4354af1 100644 --- a/.env.example +++ b/.env.example @@ -10,17 +10,14 @@ # should be updated accordingly. # Drizzle -# Get the Database URL from the "prisma" dropdown selector in PlanetScale. # Change the query params at the end of the URL to "?ssl={"rejectUnauthorized":true}" -DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}' +DATABASE_URL= -# Next Auth -# You can generate a new secret on the command line with: -# openssl rand -base64 32 -# https://next-auth.js.org/configuration/options#secret -# NEXTAUTH_SECRET="" -NEXTAUTH_URL="http://localhost:3000" - -# Next Auth Discord Provider -DISCORD_CLIENT_ID="" -DISCORD_CLIENT_SECRET="" +# Clerk +CLERK_SECRET_KEY= +CLERK_WEBHOOK_SECRET= +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= +NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in +NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up +NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/home +NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/courses diff --git a/README.md b/README.md index 0683437..6ea4ddb 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,16 @@ A Duolingo alternative for language learners who seek a more personalized and ef Built with [`create-t3-app`](https://create.t3.gg/). +## Development guide + +### Clerk + Webhooks + +1. Start the dev server with `npm run dev` +2. Request a tunnel with `npx lt --port 3000` +3. Copy the link from that command +4. Go to Clerk Dashboard > Webhooks > [end point] +5. Edit the endpoint URL to use the copied link + `/api/webhooks/your-route-name` + ## TODO: ### Backend diff --git a/package-lock.json b/package-lock.json index 1c68ca3..b9b67db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "react-dom": "18.2.0", "server-only": "^0.0.1", "superjson": "^2.2.1", + "svix": "^1.15.0", "zod": "^3.22.4" }, "devDependencies": { @@ -36,6 +37,7 @@ "dotenv-cli": "^7.3.0", "drizzle-kit": "^0.19.3", "eslint": "^8.54.0", + "localtunnel": "^2.0.2", "mysql2": "^3.6.1", "postcss": "^8.4.31", "prettier": "^3.1.0", @@ -1007,6 +1009,11 @@ "node": ">=16" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -1660,6 +1667,15 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1890,6 +1906,17 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2288,6 +2315,12 @@ "integrity": "sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==", "dev": true }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/es5-ext": { "version": "0.10.62", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", @@ -2314,6 +2347,11 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -2657,6 +2695,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -2726,6 +2769,26 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -2790,6 +2853,15 @@ "is-property": "^1.0.2" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-tsconfig": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", @@ -3039,6 +3111,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3204,6 +3285,41 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/localtunnel": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz", + "integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==", + "dev": true, + "dependencies": { + "axios": "0.21.4", + "debug": "4.3.2", + "openurl": "1.1.1", + "yargs": "17.1.1" + }, + "bin": { + "lt": "bin/lt.js" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/localtunnel/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3533,6 +3649,25 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.0.1.tgz", @@ -3580,6 +3715,12 @@ "wrappy": "1" } }, + "node_modules/openurl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", + "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", + "dev": true + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -3988,6 +4129,11 @@ "node": ">=6.0.0" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4068,6 +4214,20 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4316,6 +4476,20 @@ "node": ">=10.0.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4470,6 +4644,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svix": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.15.0.tgz", + "integrity": "sha512-oV11/VIpD77QymPEIjGr8XvQwcJxPIRO8XVpWJb33ZX2qs1q7jYlVaSJ6ABYThKbmnxIGyJr5+RpchVOSE7pZg==", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "es6-promise": "^4.2.4", + "fast-sha256": "^1.3.0", + "svix-fetch": "^3.0.0", + "url-parse": "^1.4.3" + } + }, + "node_modules/svix-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/svix-fetch/-/svix-fetch-3.0.0.tgz", + "integrity": "sha512-rcADxEFhSqHbraZIsjyZNh4TF6V+koloX1OzZ+AQuObX9mZ2LIMhm1buZeuc5BIZPftZpJCMBsSiBaeszo9tRw==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/swr": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", @@ -4597,6 +4792,11 @@ "to-no-case": "^1.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -4707,6 +4907,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -4745,6 +4954,25 @@ "tslib": "^2.4.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.19", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz", + "integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4766,12 +4994,38 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -4787,6 +5041,33 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz", + "integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 431e8cf..21eac4c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-dom": "18.2.0", "server-only": "^0.0.1", "superjson": "^2.2.1", + "svix": "^1.15.0", "zod": "^3.22.4" }, "devDependencies": { @@ -41,6 +42,7 @@ "dotenv-cli": "^7.3.0", "drizzle-kit": "^0.19.3", "eslint": "^8.54.0", + "localtunnel": "^2.0.2", "mysql2": "^3.6.1", "postcss": "^8.4.31", "prettier": "^3.1.0", diff --git a/src/app/api/webhooks/sync-user-id/createUser.ts b/src/app/api/webhooks/sync-user-id/createUser.ts new file mode 100644 index 0000000..6ce03d0 --- /dev/null +++ b/src/app/api/webhooks/sync-user-id/createUser.ts @@ -0,0 +1,10 @@ +import { db } from "~/server/db"; +import { user } from "~/server/db/schema"; + +export async function createUser(id: string) { + await db.insert(user).values({ + // userId: id, + // activeLanguageId: "asdf", + }); + throw new Error("Function not implemented."); +} diff --git a/src/app/api/webhooks/sync-user-id/route.ts b/src/app/api/webhooks/sync-user-id/route.ts new file mode 100644 index 0000000..ce9f3b1 --- /dev/null +++ b/src/app/api/webhooks/sync-user-id/route.ts @@ -0,0 +1,101 @@ +import { Webhook } from "svix"; +import { headers } from "next/headers"; +import { type WebhookEvent } from "@clerk/nextjs/server"; +import { env } from "~/env"; +import { db } from "~/server/db"; +import { user } from "~/server/db/schema"; +import { eq } from "drizzle-orm"; + +export async function POST(req: Request) { + // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook + const WEBHOOK_SECRET = env.CLERK_WEBHOOK_SECRET; + + if (!WEBHOOK_SECRET) { + throw new Error( + "Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local", + ); + } + + // Get the headers + const headerPayload = headers(); + const svix_id = headerPayload.get("svix-id"); + const svix_timestamp = headerPayload.get("svix-timestamp"); + const svix_signature = headerPayload.get("svix-signature"); + + // If there are no headers, error out + if (!svix_id || !svix_timestamp || !svix_signature) { + return new Response("Error occurred -- no svix headers", { + status: 400, + }); + } + + // Get the body + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const payload = await req.json(); + const body = JSON.stringify(payload); + + // Create a new Svix instance with your secret. + const wh = new Webhook(WEBHOOK_SECRET); + + let evt: WebhookEvent; + + // Verify the payload with the headers + try { + evt = wh.verify(body, { + "svix-id": svix_id, + "svix-timestamp": svix_timestamp, + "svix-signature": svix_signature, + }) as WebhookEvent; + } catch (err) { + console.error("Error verifying webhook:", err); + return new Response("Error occurred", { + status: 400, + }); + } + + // Get the ID and type + const { id } = evt.data; + const eventType = evt.type; + + if (!id) { + return new Response(`Error occurred -- no user ID found: '${eventType}'`, { + status: 400, + }); + } + + switch (eventType) { + case "user.created": + return await createUser(id); + case "user.deleted": + return await deleteUser(id); + default: + return new Response( + `Error occurred -- unexpected event type: '${eventType}'`, + { status: 400 }, + ); + } +} + +async function createUser(id: string): Promise { + const { rowsAffected } = await db.insert(user).values({ userId: id }); + + if (rowsAffected === 1) { + return new Response("Successfully created new user", { status: 200 }); + } + + return new Response("Error occurred -- could not create new user", { + status: 400, + }); +} + +async function deleteUser(id: string): Promise { + const { rowsAffected } = await db.delete(user).where(eq(user.userId, id)); + + if (rowsAffected === 1) { + return new Response("Successfully deleted user", { status: 200 }); + } + + return new Response("Error occurred -- could not delete user", { + status: 400, + }); +} diff --git a/src/env.js b/src/env.js index c1f991f..ba3923d 100644 --- a/src/env.js +++ b/src/env.js @@ -18,6 +18,7 @@ export const env = createEnv({ .enum(["development", "test", "production"]) .default("development"), CLERK_SECRET_KEY: z.string(), + CLERK_WEBHOOK_SECRET: z.string(), }, /** @@ -41,6 +42,7 @@ export const env = createEnv({ DATABASE_URL: process.env.DATABASE_URL, NODE_ENV: process.env.NODE_ENV, CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, + CLERK_WEBHOOK_SECRET: process.env.CLERK_WEBHOOK_SECRET, NEXT_PUBLIC_CLERK_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL, NEXT_PUBLIC_CLERK_SIGN_UP_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL, NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL: diff --git a/src/middleware.ts b/src/middleware.ts index d40b232..4b8d84f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -4,7 +4,7 @@ import { authMiddleware } from "@clerk/nextjs"; // Please edit this to allow other routes to be public as needed. // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware export default authMiddleware({ - publicRoutes: ["/"], + publicRoutes: ["/", "/api/webhooks(.*)"], }); export const config = { diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index cd0d500..207f240 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -16,7 +16,7 @@ export const mysqlTable = mysqlTableCreator((name) => `flurry_${name}`); export const user = mysqlTable("user", { userId: varchar("userId", { length: 255 }).unique().primaryKey(), - activeLanguageId: bigint("activeLanguageId", { mode: "bigint" }).notNull(), + activeLanguageId: bigint("activeLanguageId", { mode: "bigint" }), // .references(() => language.languageId), });