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