From 39e80bcacaf2088049bfd286dc57f44725c9d53f Mon Sep 17 00:00:00 2001 From: Eva Decker Date: Thu, 3 Oct 2024 19:41:41 -0400 Subject: [PATCH] feat: Replace Magic Links with Password sign-in flow (#121) --- .changeset/nervous-doors-sparkle.md | 5 + convex/_generated/api.d.ts | 2 + convex/auth.ts | 14 +- convex/passwordReset.ts | 23 +++ package.json | 2 + pnpm-lock.yaml | 182 ++++++++++++++++ src/components/AppHeader/AppHeader.tsx | 10 +- src/components/Button/Button.tsx | 48 ++++- src/components/Calendar/Calendar.tsx | 24 +-- src/components/ComboBox/ComboBox.tsx | 2 +- src/components/SearchField/SearchField.tsx | 6 +- src/components/Separator/Separator.tsx | 2 +- src/components/TextField/TextField.tsx | 26 ++- src/components/Tooltip/Tooltip.tsx | 7 +- .../_authenticated/admin/fields/index.tsx | 9 +- .../_authenticated/admin/forms/index.tsx | 9 +- .../_authenticated/admin/quests/index.tsx | 9 +- src/routes/_authenticated/quests/$questId.tsx | 13 +- src/routes/_unauthenticated/login.tsx | 194 ++++++++++++++---- 19 files changed, 491 insertions(+), 96 deletions(-) create mode 100644 .changeset/nervous-doors-sparkle.md create mode 100644 convex/passwordReset.ts diff --git a/.changeset/nervous-doors-sparkle.md b/.changeset/nervous-doors-sparkle.md new file mode 100644 index 0000000..b387fba --- /dev/null +++ b/.changeset/nervous-doors-sparkle.md @@ -0,0 +1,5 @@ +--- +"namesake": minor +--- + +Replace magic link signin with password form diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 4e64ffc..875ba88 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -20,6 +20,7 @@ import type * as constants from "../constants.js"; import type * as forms from "../forms.js"; import type * as helpers from "../helpers.js"; import type * as http from "../http.js"; +import type * as passwordReset from "../passwordReset.js"; import type * as questFields from "../questFields.js"; import type * as questSteps from "../questSteps.js"; import type * as quests from "../quests.js"; @@ -43,6 +44,7 @@ declare const fullApi: ApiFromModules<{ forms: typeof forms; helpers: typeof helpers; http: typeof http; + passwordReset: typeof passwordReset; questFields: typeof questFields; questSteps: typeof questSteps; quests: typeof quests; diff --git a/convex/auth.ts b/convex/auth.ts index 1a785e4..33b54e2 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -1,15 +1,11 @@ -import Resend from "@auth/core/providers/resend"; +import { Password } from "@convex-dev/auth/providers/Password"; import { convexAuth } from "@convex-dev/auth/server"; import type { MutationCtx } from "./_generated/server"; +import { ResendOTPPasswordReset } from "./passwordReset"; import { getUserByEmail } from "./users"; export const { auth, signIn, signOut, store } = convexAuth({ - providers: [ - Resend({ - apiKey: process.env.AUTH_RESEND_KEY, - from: process.env.AUTH_EMAIL ?? "Namesake ", - }), - ], + providers: [Password({ reset: ResendOTPPasswordReset })], callbacks: { async createOrUpdateUser(ctx: MutationCtx, args) { @@ -34,5 +30,9 @@ export const { auth, signIn, signOut, store } = convexAuth({ theme: "system", }); }, + + async redirect({ redirectTo }) { + return redirectTo; + }, }, }); diff --git a/convex/passwordReset.ts b/convex/passwordReset.ts new file mode 100644 index 0000000..65aa494 --- /dev/null +++ b/convex/passwordReset.ts @@ -0,0 +1,23 @@ +import Resend from "@auth/core/providers/resend"; +import { alphabet, generateRandomString } from "oslo/crypto"; +import { Resend as ResendAPI } from "resend"; + +export const ResendOTPPasswordReset = Resend({ + id: "resend-otp", + apiKey: process.env.AUTH_RESEND_KEY, + async generateVerificationToken() { + return generateRandomString(8, alphabet("0-9")); + }, + async sendVerificationRequest({ identifier: email, provider, token }) { + const resend = new ResendAPI(provider.apiKey); + const { error } = await resend.emails.send({ + from: "Namesake ", + to: [email], + subject: "Reset your Namesake password", + text: `Your password reset code is: ${token}`, + }); + if (error) { + throw new Error("Failed to send password reset email"); + } + }, +}); diff --git a/package.json b/package.json index aa5347a..701800f 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "convex": "^1.16.3", "convex-helpers": "^0.1.58", "next-themes": "^0.3.0", + "oslo": "^1.2.1", "postcss": "^8.4.47", "pretty-bytes": "^6.1.1", "react": "^18.3.1", @@ -52,6 +53,7 @@ "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", "react-markdown": "^9.0.1", + "resend": "^4.0.0", "storybook": "^8.3.4", "tailwind-variants": "^0.2.1", "tailwindcss": "^3.4.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6aef9be..561b2c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + oslo: + specifier: ^1.2.1 + version: 1.2.1 postcss: specifier: ^8.4.47 version: 8.4.47 @@ -75,6 +78,9 @@ importers: react-markdown: specifier: ^9.0.1 version: 9.0.1(@types/react@18.3.10)(react@18.3.1) + resend: + specifier: ^4.0.0 + version: 4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) storybook: specifier: ^8.3.4 version: 8.3.4 @@ -1534,6 +1540,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -2252,6 +2261,13 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + '@react-email/render@0.0.17': + resolution: {integrity: sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + '@react-hook/intersection-observer@3.1.2': resolution: {integrity: sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==} peerDependencies: @@ -2644,6 +2660,9 @@ packages: '@scena/react-ruler@0.8.1': resolution: {integrity: sha512-r9FbtF1kbs5CRrK7avVGcRvhJitk8BzzOUUV2uIEjDxS2dBqCOrUD2e09FXwPdv3hcFvVmGcLYeuNrjl4P7ddA==} + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@stitches/core@1.2.8': resolution: {integrity: sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==} @@ -3086,6 +3105,10 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -3496,6 +3519,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3526,6 +3553,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -3744,6 +3774,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3841,6 +3875,11 @@ packages: peerDependencies: react: '>=16.12.0' + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -4020,6 +4059,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4298,9 +4340,16 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + html-url-attributes@3.0.0: resolution: {integrity: sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -4355,6 +4404,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.2.3: resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==} @@ -4558,6 +4610,11 @@ packages: jose@5.9.3: resolution: {integrity: sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg==} + js-beautify@1.15.1: + resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + engines: {node: '>=14'} + hasBin: true + js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} @@ -4618,6 +4675,9 @@ packages: keycon@1.4.0: resolution: {integrity: sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A==} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + lexical@0.17.1: resolution: {integrity: sha512-72/MhR7jqmyqD10bmJw8gztlCm4KDDT+TPtU4elqXrEvHoO5XENi34YAEUD9gIkPfqSwyLa9mwAX1nKzIr5xEA==} @@ -4952,6 +5012,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -5048,6 +5112,11 @@ packages: engines: {node: '>=6'} hasBin: true + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -5189,6 +5258,9 @@ packages: parse5@7.1.2: resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -5250,6 +5322,9 @@ packages: resolution: {integrity: sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==} engines: {node: '>=18'} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -5566,6 +5641,9 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5937,6 +6015,9 @@ packages: react-moveable@0.30.3: resolution: {integrity: sha512-kSE0s9at2ku0sSlzrCzuRWFOfd0V9JQR6G5gsjiWytPAOsBmCl4p4KhqGvoyTcCxaMg9t/q73WZ6Es9yhOifKg==} + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + react-remove-scroll-bar@2.3.6: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -6029,6 +6110,10 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resend@4.0.0: + resolution: {integrity: sha512-rDX0rspl/XcmC2JV2V5obQvRX2arzxXUvNFUDMOv5ObBLR68+7kigCOysb7+dlkb0JE3erhQG0nHrbBt/ZCWIg==} + engines: {node: '>=18'} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -6104,6 +6189,9 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + selecto@1.26.3: resolution: {integrity: sha512-gZHgqMy5uyB6/2YDjv3Qqaf7bd2hTDOpPdxXlrez4R3/L0GiEWDCFaUfrflomgqdb3SxHF2IXY0Jw0EamZi7cw==} @@ -8496,6 +8584,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@one-ini/wasm@0.1.1': {} + '@open-draft/deferred-promise@2.2.0': {} '@panva/hkdf@1.2.1': {} @@ -9559,6 +9649,14 @@ snapshots: '@swc/helpers': 0.5.12 react: 18.3.1 + '@react-email/render@0.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + html-to-text: 9.0.5 + js-beautify: 1.15.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-promise-suspense: 0.3.4 + '@react-hook/intersection-observer@3.1.2(react@18.3.1)': dependencies: '@react-hook/passive-layout-effect': 1.2.1(react@18.3.1) @@ -10038,6 +10136,11 @@ snapshots: '@daybrush/utils': 1.13.0 framework-utils: 1.1.0 + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@stitches/core@1.2.8': {} '@storybook/addon-themes@8.3.4(storybook@8.3.4)': @@ -10594,6 +10697,8 @@ snapshots: abbrev@1.1.1: optional: true + abbrev@2.0.0: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -11101,6 +11206,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@12.1.0: {} commander@4.1.1: {} @@ -11121,6 +11228,11 @@ snapshots: concat-map@0.0.1: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + console-control-strings@1.1.0: optional: true @@ -11346,6 +11458,8 @@ snapshots: deep-eql@5.0.2: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.0 @@ -11434,6 +11548,13 @@ snapshots: react-is: 17.0.2 tslib: 2.7.0 + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.3 + ee-first@1.1.1: {} electron-to-chromium@1.5.13: {} @@ -11754,6 +11875,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-deep-equal@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -12106,8 +12229,23 @@ snapshots: html-tags@3.3.1: {} + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + html-url-attributes@3.0.0: {} + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -12168,6 +12306,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + inline-style-parser@0.2.3: {} internal-slot@1.0.7: @@ -12354,6 +12494,14 @@ snapshots: jose@5.9.3: {} + js-beautify@1.15.1: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + js-cookie@3.0.5: {} js-tokens@4.0.0: {} @@ -12431,6 +12579,8 @@ snapshots: '@scena/event-emitter': 1.0.5 keycode: 2.2.1 + leac@0.6.0: {} + lexical@0.17.1: {} lib0@0.2.98: @@ -13021,6 +13171,10 @@ snapshots: dependencies: brace-expansion: 1.1.11 + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.1 @@ -13096,6 +13250,10 @@ snapshots: abbrev: 1.1.1 optional: true + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -13250,6 +13408,11 @@ snapshots: entities: 4.5.0 optional: true + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -13294,6 +13457,8 @@ snapshots: - encoding - supports-color + peberminta@0.9.0: {} + performance-now@2.1.0: {} picocolors@1.0.1: {} @@ -13556,6 +13721,8 @@ snapshots: property-information@6.5.0: {} + proto-list@1.2.4: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -14184,6 +14351,10 @@ snapshots: overlap-area: 1.1.0 react-css-styled: 1.1.9 + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + react-remove-scroll-bar@2.3.6(@types/react@18.3.10)(react@18.3.1): dependencies: react: 18.3.1 @@ -14325,6 +14496,13 @@ snapshots: requires-port@1.0.0: optional: true + resend@4.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@react-email/render': 0.0.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom + resize-observer-polyfill@1.5.1: {} resolve-from@5.0.0: {} @@ -14419,6 +14597,10 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.0 + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + selecto@1.26.3: dependencies: '@daybrush/utils': 1.13.0 diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx index 6ea40dc..9f28a03 100644 --- a/src/components/AppHeader/AppHeader.tsx +++ b/src/components/AppHeader/AppHeader.tsx @@ -1,7 +1,6 @@ import { useAuthActions } from "@convex-dev/auth/react"; import { api } from "@convex/_generated/api"; import { RiAccountCircleFill } from "@remixicon/react"; -import { redirect } from "@tanstack/react-router"; import { Authenticated, useQuery } from "convex/react"; import { Button } from "../Button"; import { Link } from "../Link"; @@ -15,7 +14,6 @@ export const AppHeader = () => { const handleSignOut = () => { signOut(); - redirect({ to: "/", throw: true }); }; return ( @@ -27,9 +25,11 @@ export const AppHeader = () => { {isAdmin && Admin}
- + + + diff --git a/src/components/SearchField/SearchField.tsx b/src/components/SearchField/SearchField.tsx index 160b86c..afad43a 100644 --- a/src/components/SearchField/SearchField.tsx +++ b/src/components/SearchField/SearchField.tsx @@ -44,9 +44,9 @@ export function SearchField({ + size="small" + icon={RiCloseLine} + /> {description && {description}} {errorMessage} diff --git a/src/components/Separator/Separator.tsx b/src/components/Separator/Separator.tsx index ebff2d4..1e62ade 100644 --- a/src/components/Separator/Separator.tsx +++ b/src/components/Separator/Separator.tsx @@ -5,7 +5,7 @@ import { import { tv } from "tailwind-variants"; const styles = tv({ - base: "bg-gray-3 dark:bg-gray-6 forced-colors:bg-[ButtonBorder]", + base: "border-gray-dim forced-colors:bg-[ButtonBorder]", variants: { orientation: { horizontal: "h-px w-full", diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 2c3ce91..769b029 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -1,9 +1,12 @@ +import { RiEyeCloseLine, RiEyeLine } from "@remixicon/react"; +import { useState } from "react"; import { TextField as AriaTextField, type TextFieldProps as AriaTextFieldProps, type ValidationResult, } from "react-aria-components"; import { tv } from "tailwind-variants"; +import { Button } from "../Button"; import { FieldDescription, FieldError, @@ -12,6 +15,7 @@ import { Label, fieldBorderStyles, } from "../Field"; +import { Tooltip, TooltipTrigger } from "../Tooltip"; import { composeTailwindRenderProps, focusRing } from "../utils"; const inputStyles = tv({ @@ -25,7 +29,7 @@ const inputStyles = tv({ export interface TextFieldProps extends AriaTextFieldProps { label?: string; - description?: string; + description?: string | React.ReactNode; icon?: React.ReactNode; rightIcon?: React.ReactNode; errorMessage?: string | ((validation: ValidationResult) => string); @@ -39,6 +43,8 @@ export function TextField({ errorMessage, ...props }: TextFieldProps) { + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + return ( {label && } {icon} {rightIcon} + {props.type === "password" && ( + + + + + + - - ); -}; -const SignIn = () => { - const [step, setStep] = useState<"signIn" | "linkSent">("signIn"); + if (!result.signingIn) { + setError( + `Couldn't ${flow === "signIn" ? "sign in" : "register"}. Try again.`, + ); + } - return ( - - {step === "signIn" ? ( - setStep("linkSent")} /> - ) : ( -
- - Check your email. A sign-in link has been sent to your email - address. - - + if (result.redirect) { + throw redirect({ to: result.redirect.toString() }); + } + }} + > + {error && {error}} + + + +
+ + or +
- )} + + + ); }; @@ -80,6 +89,107 @@ const ClosedSignups = () => ( ); +const ForgotPassword = ({ onBack }: { onBack: () => void }) => { + const { signIn } = useAuthActions(); + const [code, setCode] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [email, setEmail] = useState(""); + const [didSendCode, setDidSendCode] = useState(false); + const [step, setStep] = useState<"forgot" | { email: string }>("forgot"); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + return step === "forgot" ? ( + +
{ + event.preventDefault(); + setIsSubmitting(true); + const formData = new FormData(event.currentTarget); + formData.append("flow", "reset"); + + signIn("password", formData) + .then(() => { + setStep({ email: formData.get("email") as string }); + setError(null); + setDidSendCode(true); + }) + .catch((error) => { + console.error(error); + setError(`Couldn't send code. Try again.`); + setIsSubmitting(false); + }) + .finally(() => setIsSubmitting(false)); + }} + > +

Reset password

+ {error && {error}} + + + + +
+ ) : ( + +
{ + event.preventDefault(); + setIsSubmitting(true); + const formData = new FormData(event.currentTarget); + formData.append("flow", "reset-verification"); + formData.append("redirectTo", "/quests"); + formData.append("email", step.email); + + try { + const result = await signIn("password", formData); + if (result.redirect) { + throw redirect({ to: result.redirect.toString() }); + } + } catch (error) { + console.error(error); + setDidSendCode(false); + setError("Couldn’t reset password. Try again."); + setIsSubmitting(false); + } + }} + > + {didSendCode && ( + + Code emailed. Paste the code below and enter your new password. + + )} + {error && {error}} + + + + +
+ ); +}; + function LoginRoute() { const isClosed = process.env.NODE_ENV === "production";