From 06b5381bfc5dfd5aad252d214e1e08850465f010 Mon Sep 17 00:00:00 2001 From: Zachary Blasczyk <77289967+zacharyblasczyk@users.noreply.github.com> Date: Sun, 13 Oct 2024 09:57:19 -0500 Subject: [PATCH] Improve Signin and Signup Flow (#133) --- apps/webservice/package.json | 2 +- .../src/app/(auth)/login/LoginCard.tsx | 37 ++++++++++++--- .../src/app/(auth)/sign-up/SignUpCard.tsx | 45 +++++++++++++------ .../src/app/(auth)/sign-up/page.tsx | 2 +- apps/webshell-router/package.json | 2 +- package.json | 1 + packages/auth/package.json | 2 +- packages/auth/src/config.ts | 2 + packages/auth/src/utils/credentials.ts | 7 +-- packages/validators/src/auth/index.ts | 4 ++ pnpm-lock.yaml | 43 +++++++++--------- pnpm-workspace.yaml | 1 + 12 files changed, 101 insertions(+), 47 deletions(-) diff --git a/apps/webservice/package.json b/apps/webservice/package.json index 478a28de..864ee2e4 100644 --- a/apps/webservice/package.json +++ b/apps/webservice/package.json @@ -53,7 +53,7 @@ "match-sorter": "^6.3.4", "murmurhash": "^2.0.1", "next": "catalog:", - "next-auth": "5.0.0-beta.18", + "next-auth": "catalog:", "next-themes": "^0.3.0", "pretty-ms": "^9.0.0", "randomcolor": "^0.6.2", diff --git a/apps/webservice/src/app/(auth)/login/LoginCard.tsx b/apps/webservice/src/app/(auth)/login/LoginCard.tsx index 90c64669..32a5a675 100644 --- a/apps/webservice/src/app/(auth)/login/LoginCard.tsx +++ b/apps/webservice/src/app/(auth)/login/LoginCard.tsx @@ -1,8 +1,10 @@ "use client"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; import { IconBrandGoogle, IconLock } from "@tabler/icons-react"; import { signIn } from "next-auth/react"; -import { z } from "zod"; +import { useLocalStorage } from "react-use"; import { Button } from "@ctrlplane/ui/button"; import { @@ -12,25 +14,44 @@ import { FormItem, FormLabel, FormMessage, + FormRootError, useForm, } from "@ctrlplane/ui/form"; import { Input } from "@ctrlplane/ui/input"; +import * as schema from "@ctrlplane/validators/auth"; export const LoginCard: React.FC<{ isGoogleEnabled: boolean; isOidcEnabled: boolean; }> = ({ isGoogleEnabled, isOidcEnabled }) => { + const router = useRouter(); const form = useForm({ - schema: z.object({ - email: z.string().email(), - password: z.string().min(8), - }), + schema: schema.signInSchema, defaultValues: { email: "", password: "" }, }); - const onSubmit = form.handleSubmit((data, event) => { + const [lastEnteredEmail, setLastEnteredEmail] = useLocalStorage( + "lastEnteredEmail", + "", + ); + + useEffect(() => { + if (lastEnteredEmail) form.setValue("email", lastEnteredEmail); + const subscription = form.watch(({ email }) => + setLastEnteredEmail(email ?? ""), + ); + return () => subscription.unsubscribe(); + }, [form, lastEnteredEmail, setLastEnteredEmail]); + + const onSubmit = form.handleSubmit(async (data, event) => { event?.preventDefault(); - signIn("credentials", { ...data, callbackUrl: "/" }); + await signIn("credentials", { ...data }) + .then(() => router.push("/")) + .catch(() => { + form.setError("root", { + message: "Sign in failed. Please try again.", + }); + }); }); return ( @@ -69,6 +90,8 @@ export const LoginCard: React.FC<{ )} /> + + diff --git a/apps/webservice/src/app/(auth)/sign-up/SignUpCard.tsx b/apps/webservice/src/app/(auth)/sign-up/SignUpCard.tsx index 4b58f3f5..f9afb35f 100644 --- a/apps/webservice/src/app/(auth)/sign-up/SignUpCard.tsx +++ b/apps/webservice/src/app/(auth)/sign-up/SignUpCard.tsx @@ -1,8 +1,9 @@ "use client"; +import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { signIn } from "next-auth/react"; -import { z } from "zod"; +import { useLocalStorage } from "react-use"; import { Button } from "@ctrlplane/ui/button"; import { @@ -12,23 +13,19 @@ import { FormItem, FormLabel, FormMessage, + FormRootError, useForm, } from "@ctrlplane/ui/form"; import { Input } from "@ctrlplane/ui/input"; +import * as schema from "@ctrlplane/validators/auth"; import { api } from "~/trpc/react"; -const schema = z.object({ - name: z.string().min(1), - email: z.string().email(), - password: z.string().min(8), -}); - export const SignUpCard: React.FC = () => { const router = useRouter(); const signUp = api.user.auth.signUp.useMutation(); const form = useForm({ - schema, + schema: schema.signUpSchema, defaultValues: { name: "", email: "", @@ -36,10 +33,32 @@ export const SignUpCard: React.FC = () => { }, }); - const onSubmit = form.handleSubmit(async (data) => { - await signUp.mutateAsync(data); - await signIn("credentials", data); - router.replace("/"); + const [lastEnteredEmail, setLastEnteredEmail] = useLocalStorage( + "lastEnteredEmail", + "", + ); + + useEffect(() => { + if (lastEnteredEmail) form.setValue("email", lastEnteredEmail); + + const subscription = form.watch( + (value, { name }) => + name === "email" && setLastEnteredEmail(value.email ?? ""), + ); + return () => subscription.unsubscribe(); + }, [form, lastEnteredEmail, setLastEnteredEmail]); + + const onSubmit = form.handleSubmit((data) => { + signUp + .mutateAsync(data) + .then(() => { + signIn("credentials", data).then(() => router.push("/")); + }) + .catch(() => { + form.setError("root", { + message: "Sign up failed. Please try again.", + }); + }); }); return ( @@ -92,7 +111,7 @@ export const SignUpCard: React.FC = () => { )} /> - + - + diff --git a/apps/webshell-router/package.json b/apps/webshell-router/package.json index 04ce7426..da45f627 100644 --- a/apps/webshell-router/package.json +++ b/apps/webshell-router/package.json @@ -18,7 +18,7 @@ "express-rate-limit": "^7.3.0", "helmet": "^7.1.0", "ms": "^2.1.3", - "next-auth": "5.0.0-beta.18", + "next-auth": "catalog:", "ws": "^8.17.0", "zod": "catalog:" }, diff --git a/package.json b/package.json index 8cbbd8a5..855ea761 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "db:push": "pnpm -F db push", "db:studio": "pnpm -F db studio", "dev": "turbo dev --parallel --concurrency 30 --cache-workers 30 --filter=!./integrations/**/*", + "dev:docker": "docker compose -f docker-compose.dev.yaml up -d", "format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache", "format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache", "lint": "turbo lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache", diff --git a/packages/auth/package.json b/packages/auth/package.json index 3dc86f8e..c8fa1b03 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -27,7 +27,7 @@ "@t3-oss/env-nextjs": "catalog:", "bcryptjs": "^2.4.3", "next": "catalog:", - "next-auth": "5.0.0-beta.18", + "next-auth": "catalog:", "react": "18.3.1", "react-dom": "18.3.1", "zod": "catalog:" diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index 239d9cd5..c6411043 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -77,6 +77,8 @@ export const authConfig: NextAuthConfig = { pages: { signIn: "/login" }, session: { strategy: "jwt" }, + secret: env.AUTH_SECRET, + adapter: DrizzleAdapter(db, { usersTable: schema.user, accountsTable: schema.account, diff --git a/packages/auth/src/utils/credentials.ts b/packages/auth/src/utils/credentials.ts index 9a349fec..8154f8bc 100644 --- a/packages/auth/src/utils/credentials.ts +++ b/packages/auth/src/utils/credentials.ts @@ -13,8 +13,9 @@ const getUserByEmail = (email: string) => export const getUserByCredentials = async (email: string, password: string) => { const user = await getUserByEmail(email); - if (user == null) return null; + if (user == null) return new Error("Invalid credentials"); const { passwordHash } = user; - if (passwordHash == null) return null; - return compareSync(password, passwordHash) ? user : null; + if (passwordHash == null) return new Error("Invalid credentials"); + const isPasswordCorrect = compareSync(password, passwordHash); + return isPasswordCorrect ? user : new Error("Invalid credentials"); }; diff --git a/packages/validators/src/auth/index.ts b/packages/validators/src/auth/index.ts index c7863e54..cab7f112 100644 --- a/packages/validators/src/auth/index.ts +++ b/packages/validators/src/auth/index.ts @@ -12,6 +12,10 @@ export const signInSchema = z.object({ .max(32, "Password must be less than 32 characters"), }); +export const signUpSchema = signInSchema.extend({ + name: z.string().min(1, "Name is required"), +}); + export enum Permission { IamSetPolicy = "iam.setIamPolicy", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d5dff7e..5a1b22f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ catalogs: next: specifier: ^14.2.13 version: 14.2.13 + next-auth: + specifier: 5.0.0-beta.22 + version: 5.0.0-beta.22 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -447,8 +450,8 @@ importers: specifier: 'catalog:' version: 14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: - specifier: 5.0.0-beta.18 - version: 5.0.0-beta.18(next@14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 5.0.0-beta.22(next@14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -592,8 +595,8 @@ importers: specifier: ^2.1.3 version: 2.1.3 next-auth: - specifier: 5.0.0-beta.18 - version: 5.0.0-beta.18(next@14.2.14(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 5.0.0-beta.22(next@14.2.14(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) ws: specifier: ^8.17.0 version: 8.18.0 @@ -1003,8 +1006,8 @@ importers: specifier: 'catalog:' version: 14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: - specifier: 5.0.0-beta.18 - version: 5.0.0-beta.18(next@14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + specifier: 'catalog:' + version: 5.0.0-beta.22(next@14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -1538,8 +1541,8 @@ packages: bullmq: optional: true - '@auth/core@0.31.0': - resolution: {integrity: sha512-UKk3psvA1cRbk4/c9CkpWB8mdWrkKvzw0DmEYRsWolUQytQ2cRqx+hYuV6ZCsngw/xbj9hpmkZmAZEyq2g4fMg==} + '@auth/core@0.34.1': + resolution: {integrity: sha512-tuYU2VIbI8rFbkSwP710LmybB2FXJsPN7j3sjRVfN9SXVQBK2ej6LdewQaofpBGp4Mk+cC2UeiGNH0or4tgaeA==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 @@ -1552,8 +1555,8 @@ packages: nodemailer: optional: true - '@auth/core@0.34.1': - resolution: {integrity: sha512-tuYU2VIbI8rFbkSwP710LmybB2FXJsPN7j3sjRVfN9SXVQBK2ej6LdewQaofpBGp4Mk+cC2UeiGNH0or4tgaeA==} + '@auth/core@0.35.3': + resolution: {integrity: sha512-g6qfiqU4OtyvIEZ8J7UoIwAxEnNnLJV0/f/DW41U+4G5nhBlaCrnKhawJIJpU0D3uavXLeDT3B0BkjtiimvMDA==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 @@ -8916,14 +8919,14 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - next-auth@5.0.0-beta.18: - resolution: {integrity: sha512-x55L8wZb8PcPGCYA3e/l9tdpd7YL3FDuhas4W8pxq3PjrWJ9OoDxNN0otK9axJamJBbBgjfzTJjVQB6hXoe0ZQ==} + next-auth@5.0.0-beta.22: + resolution: {integrity: sha512-QGBo9HGOjmnJBHGXvtFztl0tM5tL0porDlk74HVoCCzXd986ApOlIW3EmiCuho7YzEopgkFiwwmcXpoCrHAtYw==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 - next: ^14 + next: ^14.0.0-0 || ^15.0.0-0 nodemailer: ^6.6.5 - react: ^18.2.0 + react: ^18.2.0 || ^19.0.0-0 peerDependenciesMeta: '@simplewebauthn/browser': optional: true @@ -11790,7 +11793,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@auth/core@0.31.0': + '@auth/core@0.34.1': dependencies: '@panva/hkdf': 1.2.1 '@types/cookie': 0.6.0 @@ -11800,7 +11803,7 @@ snapshots: preact: 10.11.3 preact-render-to-string: 5.2.3(preact@10.11.3) - '@auth/core@0.34.1': + '@auth/core@0.35.3': dependencies: '@panva/hkdf': 1.2.1 '@types/cookie': 0.6.0 @@ -20577,15 +20580,15 @@ snapshots: netmask@2.0.2: {} - next-auth@5.0.0-beta.18(next@14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-auth@5.0.0-beta.22(next@14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - '@auth/core': 0.31.0 + '@auth/core': 0.35.3 next: 14.2.13(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 - next-auth@5.0.0-beta.18(next@14.2.14(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + next-auth@5.0.0-beta.22(next@14.2.14(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: - '@auth/core': 0.31.0 + '@auth/core': 0.35.3 next: 14.2.14(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0f5e2aaa..9c3e3298 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,6 +19,7 @@ catalog: "tsx": ^4.19.1 "lodash-es": ^4.17.21 "next": ^14.2.13 + "next-auth": "5.0.0-beta.22" "@next/eslint-plugin-next": ^14.2.6 "bullmq": ^5.15.0