Fix session handling and update auth pages

This commit is contained in:
Fergal Moran
2024-09-06 11:48:43 +01:00
parent ad67ae700e
commit c7cbc3d8c0
10 changed files with 241 additions and 196 deletions

View File

@@ -1,3 +1,3 @@
{
"workbench.colorTheme": "City Lights"
"workbench.colorTheme": "Tinacious Design (High Contrast)"
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -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"

View File

@@ -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 <div className="min-h-screen">{children}</div>;
return (
<div className="min-h-screen">
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 md:left-8 md:top-8",
)}
>
<>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
Back
</>
</Link>
{children}
</div>
</div>
);
}

View File

@@ -10,40 +10,22 @@ import { buttonVariants } from "@/components/ui/button";
const RegisterPage: React.FC = () => {
return (
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 md:left-8 md:top-8",
)}
>
<>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
Back
</>
</Link>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">
Welcome back
</h1>
<p className="text-sm text-muted-foreground">
Enter your email to sign in to your account
</p>
</div>
<RegistrationForm />
<SocialLogin />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/register"
className="hover:text-brand underline underline-offset-4"
>
Don&apos;t have an account? Sign Up
</Link>
</p>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">Welcome</h1>
<p className="text-sm text-muted-foreground">Register for an account</p>
</div>
<RegistrationForm />
<SocialLogin />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/signin"
className="hover:text-brand underline underline-offset-4"
>
Already have an account? Login?
</Link>
</p>
</div>
);
};

View File

@@ -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 (
<div className="flex min-h-full flex-col justify-center py-1 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="mt-2 text-center text-3xl font-extrabold">
Sign in to your account
</h2>
<p className="mt-2 text-center text-sm">
Or
<Link href="/register" className="font-medium">
create a new account
</Link>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">Welcome back</h1>
<p className="text-sm text-muted-foreground">
Enter your email to sign in to your account
</p>
</div>
<div className="mt-2 sm:mx-auto sm:w-full sm:max-w-md">
<div className="px-4 py-8 shadow sm:rounded-lg sm:px-10">
<form
className="space-y-6"
onSubmit={async (e) => {
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"
>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email address
</label>
<div className="mt-1">
<input
id="email"
type="email"
autoComplete="email"
required
value={userInfo.email}
onChange={({ target }) =>
setUserInfo({ ...userInfo, email: target.value })
}
className="input input-bordered w-full"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<div className="mt-1">
<input
id="password"
type="password"
autoComplete="current-password"
required
value={userInfo.password}
onChange={({ target }) =>
setUserInfo({ ...userInfo, password: target.value })
}
className="input input-bordered w-full"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4"
/>
<label
htmlFor="remember-me"
className="text-accent ml-2 block text-sm"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
className="text-info hover:text-primary/50 font-medium"
>
Forgot your password?
</a>
</div>
</div>
<div>
<button type="submit" className="btn btn-primary w-full">
Sign in
</button>
</div>
</form>
<div className="mt-6">
<SocialLogin />
</div>
</div>
</div>
<SignInForm />
<SocialLogin />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/register"
className="hover:text-brand underline underline-offset-4"
>
Don&apos;t have an account? Sign Up
</Link>
</p>
</div>
);
};

View File

@@ -18,6 +18,8 @@ export default async function Home() {
<div>
<a href="/signin">Sign In</a>
</div>
<div>{JSON.stringify(session, null, 2)}</div>
{session?.user && <TrendingImages />}
</div>
</main>

View File

@@ -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<typeof signInSchema>;
const SignInForm: React.FC = () => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const router = useRouter();
const form = useForm<SignInFormValues>({
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid gap-2">
<div className="grid gap-1">
<FormField
control={form.control}
name="email"
defaultValue={"fergal.moran+opengifame@gmail.com"}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
{form.formState.errors.email && (
<FormMessage>
{form.formState.errors.email.message}
</FormMessage>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
defaultValue={"secret"}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
{form.formState.errors.password && (
<FormMessage>
{form.formState.errors.password.message}
</FormMessage>
)}
</FormItem>
)}
/>
{form.formState.errors && false && (
<Alert>
<Icons.terminal className="h-4 w-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
{JSON.stringify(form.formState.errors)}
</AlertDescription>
</Alert>
)}
</div>
<Button
type="submit"
className={cn(buttonVariants())}
disabled={isLoading}
>
{isLoading && (
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
)}
Sign in
</Button>
</div>
</form>
</Form>
);
};
export default SignInForm;

View File

@@ -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];
}),
});

View File

@@ -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 };
},
}),
],