mirror of
https://github.com/fergalmoran/opengifame.git
synced 2025-12-22 09:38:44 +00:00
Fix session handling and update auth pages
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"workbench.colorTheme": "City Lights"
|
||||
"workbench.colorTheme": "Tinacious Design (High Contrast)"
|
||||
}
|
||||
42
package.json
42
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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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't have an account? Sign Up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
128
src/components/forms/auth/SignInForm.tsx
Normal file
128
src/components/forms/auth/SignInForm.tsx
Normal 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;
|
||||
@@ -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];
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user