diff --git a/.vscode/settings.json b/.vscode/settings.json index d94e71c..cae72af 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "workbench.colorTheme": "City Lights" + "workbench.colorTheme": "Tinacious Design (High Contrast)" } \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 5043618..fb08ec3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 1b22df7..867e8c4 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "start": "next start" }, "dependencies": { - "@auth/drizzle-adapter": "^1.1.0", - "@headlessui/react": "^2.1.4", + "@auth/drizzle-adapter": "^1.4.2", + "@headlessui/react": "^2.1.5", "@hookform/resolvers": "^3.9.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", @@ -45,8 +45,8 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", - "@t3-oss/env-nextjs": "^0.10.1", - "@tanstack/react-query": "^5.50.0", + "@t3-oss/env-nextjs": "^0.11.1", + "@tanstack/react-query": "^5.54.1", "@trpc/client": "^11.0.0-rc.446", "@trpc/react-query": "^11.0.0-rc.446", "@trpc/server": "^11.0.0-rc.446", @@ -62,19 +62,19 @@ "date-fns": "^3.6.0", "drizzle-orm": "^0.33.0", "embla-carousel-react": "^8.2.1", - "geist": "^1.3.0", + "geist": "^1.3.1", "input-otp": "^1.2.4", "lodash": "^4.17.21", "loglevel": "^1.9.1", "loglevel-plugin-prefix": "^0.8.4", "lucide-react": "^0.438.0", - "next": "^14.2.4", + "next": "^14.2.8", "next-auth": "^4.24.7", "next-themes": "^0.3.0", "postgres": "^3.4.4", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", - "react-day-picker": "8.10.1", + "react-day-picker": "9.0.8", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", "react-icons": "^5.3.0", @@ -85,25 +85,25 @@ "superjson": "^2.2.1", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.1", + "vaul": "^0.9.2", "zod": "^3.23.8" }, "devDependencies": { - "@types/eslint": "^8.56.10", - "@types/node": "^20.14.10", - "@types/react": "^18.3.3", + "@types/eslint": "^9.6.1", + "@types/node": "^22.5.4", + "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.1.0", - "drizzle-kit": "^0.24.0", - "eslint": "^8.57.0", - "eslint-config-next": "^14.2.4", + "@typescript-eslint/eslint-plugin": "^8.4.0", + "@typescript-eslint/parser": "^8.4.0", + "drizzle-kit": "^0.24.2", + "eslint": "^9.9.1", + "eslint-config-next": "^14.2.8", "eslint-plugin-drizzle": "^0.2.3", - "postcss": "^8.4.39", - "prettier": "^3.3.2", - "prettier-plugin-tailwindcss": "^0.6.5", - "tailwindcss": "^3.4.3", - "typescript": "^5.5.3" + "postcss": "^8.4.45", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.6", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4" }, "ct3aMetadata": { "initVersion": "7.37.0" diff --git a/src/app/(site)/(auth)/layout.tsx b/src/app/(site)/(auth)/layout.tsx index a8e6032..67c2cef 100644 --- a/src/app/(site)/(auth)/layout.tsx +++ b/src/app/(site)/(auth)/layout.tsx @@ -1,7 +1,30 @@ +import Link from "next/link"; +import { Icons } from "@/components/icons"; +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; interface AuthLayoutProps { children: React.ReactNode; } export default function AuthLayout({ children }: AuthLayoutProps) { - return
{children}
; + return ( +
+
+ + <> + + Back + + + + {children} +
+
+ ); } diff --git a/src/app/(site)/(auth)/register/page.tsx b/src/app/(site)/(auth)/register/page.tsx index 7824faf..39066b8 100644 --- a/src/app/(site)/(auth)/register/page.tsx +++ b/src/app/(site)/(auth)/register/page.tsx @@ -10,40 +10,22 @@ import { buttonVariants } from "@/components/ui/button"; const RegisterPage: React.FC = () => { return ( -
- - <> - - Back - - -
-
- -

- Welcome back -

-

- Enter your email to sign in to your account -

-
- - -

- - Don't have an account? Sign Up - -

+
+
+ +

Welcome

+

Register for an account

+ + +

+ + Already have an account? Login? + +

); }; diff --git a/src/app/(site)/(auth)/signin/page.tsx b/src/app/(site)/(auth)/signin/page.tsx index 9bb8e58..f2e7d82 100644 --- a/src/app/(site)/(auth)/signin/page.tsx +++ b/src/app/(site)/(auth)/signin/page.tsx @@ -2,121 +2,31 @@ import SocialLogin from "@/components/widgets/login/SocialLogin"; import Link from "next/link"; import React from "react"; -import { logger } from "@/lib/logger"; -import { signIn } from "next-auth/react"; +import { Icons } from "@/components/icons"; +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; +import SignInForm from "@/components/forms/auth/SignInForm"; const SignInPage = () => { - const [userInfo, setUserInfo] = React.useState({ - email: "fergal.moran+opengifame@gmail.com", - password: "secret", - }); - return ( -
-
-

- Sign in to your account -

-

- Or - - create a new account - +

+
+ +

Welcome back

+

+ Enter your email to sign in to your account

- -
-
-
{ - e.preventDefault(); - logger.debug("signin", "using", userInfo); - const result = await signIn("credentials", { - redirect: false, - email: userInfo.email, - password: userInfo.password, - }); - logger.debug("signin", "result", result); - }} - method="post" - > -
- -
- - setUserInfo({ ...userInfo, email: target.value }) - } - className="input input-bordered w-full" - /> -
-
- -
- -
- - setUserInfo({ ...userInfo, password: target.value }) - } - className="input input-bordered w-full" - /> -
-
- -
-
- - -
- - -
- -
- -
-
- -
- -
-
-
+ + +

+ + Don't have an account? Sign Up + +

); }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 71d36ad..6366cd0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -18,6 +18,8 @@ export default async function Home() { + +
{JSON.stringify(session, null, 2)}
{session?.user && }
diff --git a/src/components/forms/auth/SignInForm.tsx b/src/components/forms/auth/SignInForm.tsx new file mode 100644 index 0000000..777d341 --- /dev/null +++ b/src/components/forms/auth/SignInForm.tsx @@ -0,0 +1,128 @@ +// src/components/RegistrationForm.tsx +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Button, buttonVariants } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/trpc/react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { logger } from "@/lib/logger"; +import { Icons } from "@/components/icons"; +import { cn } from "@/lib/utils"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; +import { signIn } from "next-auth/react"; + +const signInSchema = z.object({ + email: z.string().email({ message: "Invalid email address" }), + password: z + .string() + .min(5, { message: "Password must be at least 5 characters long" }), +}); + +type SignInFormValues = z.infer; + +const SignInForm: React.FC = () => { + const [isLoading, setIsLoading] = React.useState(false); + const router = useRouter(); + const form = useForm({ + resolver: zodResolver(signInSchema), + }); + + const onSubmit = async (data: SignInFormValues) => { + setIsLoading(true); + try { + const result = await signIn("credentials", { + redirect: false, + email: data.email, + password: data.password, + }); + logger.debug("signin", "result", result); + if (result?.status === 200) { + router.push("/"); + } + } catch (error) { + logger.error("SignInForm", "error", error); + toast("Failed to signin user"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+
+ ( + + Email + + + + {form.formState.errors.email && ( + + {form.formState.errors.email.message} + + )} + + )} + /> + ( + + Password + + + + {form.formState.errors.password && ( + + {form.formState.errors.password.message} + + )} + + )} + /> + {form.formState.errors && false && ( + + + Heads up! + + {JSON.stringify(form.formState.errors)} + + + )} +
+ +
+
+ + ); +}; + +export default SignInForm; diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 6802220..0bf0d4e 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -16,18 +16,4 @@ export const authRouter = createTRPCRouter({ return user; }), - login: publicProcedure - .input(z.object({ email: z.string().email(), password: z.string().min(5) })) - .query(async ({ ctx, input }) => { - const hashedPassword = await bcrypt.hash(input.password, 10); - const user = await ctx.db - .select() - .from(users) - .where( - and(eq(users.email, input.email), eq(users.password, hashedPassword)), - ) - .limit(1); - - return user[0]; - }), }); diff --git a/src/server/auth.ts b/src/server/auth.ts index cdd6ba5..4c4414d 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -1,10 +1,8 @@ import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import CredentialsProvider from "next-auth/providers/credentials"; import { getServerSession, type DefaultSession, type NextAuthOptions, - RequestInternal, } from "next-auth"; import { type Adapter } from "next-auth/adapters"; import { @@ -13,11 +11,12 @@ import { users, verificationTokens, } from "@/server/db/schema"; -import { api } from "@/trpc/server"; import { env } from "@/env"; import { db } from "@/server/db"; import Credentials from "next-auth/providers/credentials"; +import bcrypt from "bcrypt"; +import { and, eq } from "drizzle-orm"; declare module "next-auth" { interface Session extends DefaultSession { @@ -29,13 +28,18 @@ declare module "next-auth" { export const authOptions: NextAuthOptions = { callbacks: { - session: ({ session, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), + session: ({ session, user }) => { + const s = { + ...session, + user: { + ...session.user + }, + }; + return s; + }, + }, + session: { + strategy: "jwt", }, adapter: DrizzleAdapter(db, { usersTable: users, @@ -54,14 +58,24 @@ export const authOptions: NextAuthOptions = { if (!credentials) { return null; } - const result = await api.auth.login({ - email: credentials.email, - password: credentials.password, - }); - if (!result) { + const user = await db + .select() + .from(users) + .where(and(eq(users.email, credentials.email))) + .limit(1); + + if (!user || user.length < 1) { return null; } - return { id: result.id, email: result.email }; + if ( + !(await bcrypt.compare( + credentials.password, + user[0]!.password as string, + )) + ) { + return null; + } + return { id: user[0]!.id, email: user[0]!.email }; }, }), ],