Refactor to use hooks (#436)
This commit is contained in:
parent
124efca9a1
commit
cb60f8b143
139 changed files with 8871 additions and 8726 deletions
85
app/(auth)/actions.ts
Normal file
85
app/(auth)/actions.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { createUser, getUser } from "@/db/queries";
|
||||
|
||||
import { signIn } from "./auth";
|
||||
|
||||
const authFormSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
});
|
||||
|
||||
export interface LoginActionState {
|
||||
status: "idle" | "in_progress" | "success" | "failed" | "invalid_data";
|
||||
}
|
||||
|
||||
export const login = async (
|
||||
_: LoginActionState,
|
||||
formData: FormData,
|
||||
): Promise<LoginActionState> => {
|
||||
try {
|
||||
const validatedData = authFormSchema.parse({
|
||||
email: formData.get("email"),
|
||||
password: formData.get("password"),
|
||||
});
|
||||
|
||||
await signIn("credentials", {
|
||||
email: validatedData.email,
|
||||
password: validatedData.password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
return { status: "success" };
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return { status: "invalid_data" };
|
||||
}
|
||||
|
||||
return { status: "failed" };
|
||||
}
|
||||
};
|
||||
|
||||
export interface RegisterActionState {
|
||||
status:
|
||||
| "idle"
|
||||
| "in_progress"
|
||||
| "success"
|
||||
| "failed"
|
||||
| "user_exists"
|
||||
| "invalid_data";
|
||||
}
|
||||
|
||||
export const register = async (
|
||||
_: RegisterActionState,
|
||||
formData: FormData,
|
||||
): Promise<RegisterActionState> => {
|
||||
try {
|
||||
const validatedData = authFormSchema.parse({
|
||||
email: formData.get("email"),
|
||||
password: formData.get("password"),
|
||||
});
|
||||
|
||||
let [user] = await getUser(validatedData.email);
|
||||
|
||||
if (user) {
|
||||
return { status: "user_exists" } as RegisterActionState;
|
||||
} else {
|
||||
await createUser(validatedData.email, validatedData.password);
|
||||
await signIn("credentials", {
|
||||
email: validatedData.email,
|
||||
password: validatedData.password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
return { status: "success" };
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return { status: "invalid_data" };
|
||||
}
|
||||
|
||||
return { status: "failed" };
|
||||
}
|
||||
};
|
||||
1
app/(auth)/api/auth/[...nextauth]/route.ts
Normal file
1
app/(auth)/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { GET, POST } from "@/app/(auth)/auth";
|
||||
39
app/(auth)/auth.config.ts
Normal file
39
app/(auth)/auth.config.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { NextAuthConfig } from "next-auth";
|
||||
|
||||
export const authConfig = {
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
newUser: "/",
|
||||
},
|
||||
providers: [
|
||||
// added later in auth.ts since it requires bcrypt which is only compatible with Node.js
|
||||
// while this file is also used in non-Node.js environments
|
||||
],
|
||||
callbacks: {
|
||||
authorized({ auth, request: { nextUrl } }) {
|
||||
let isLoggedIn = !!auth?.user;
|
||||
let isOnChat = nextUrl.pathname.startsWith("/");
|
||||
let isOnRegister = nextUrl.pathname.startsWith("/register");
|
||||
let isOnLogin = nextUrl.pathname.startsWith("/login");
|
||||
|
||||
if (isLoggedIn && (isOnLogin || isOnRegister)) {
|
||||
return Response.redirect(new URL("/", nextUrl));
|
||||
}
|
||||
|
||||
if (isOnRegister || isOnLogin) {
|
||||
return true; // Always allow access to register and login pages
|
||||
}
|
||||
|
||||
if (isOnChat) {
|
||||
if (isLoggedIn) return true;
|
||||
return false; // Redirect unauthenticated users to login page
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
return Response.redirect(new URL("/", nextUrl));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
} satisfies NextAuthConfig;
|
||||
53
app/(auth)/auth.ts
Normal file
53
app/(auth)/auth.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { compare } from "bcrypt-ts";
|
||||
import NextAuth, { User, Session } from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
import { getUser } from "@/db/queries";
|
||||
|
||||
import { authConfig } from "./auth.config";
|
||||
|
||||
interface ExtendedSession extends Session {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth({
|
||||
...authConfig,
|
||||
providers: [
|
||||
Credentials({
|
||||
credentials: {},
|
||||
async authorize({ email, password }: any) {
|
||||
let users = await getUser(email);
|
||||
if (users.length === 0) return null;
|
||||
let passwordsMatch = await compare(password, users[0].password!);
|
||||
if (passwordsMatch) return users[0] as any;
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({
|
||||
session,
|
||||
token,
|
||||
}: {
|
||||
session: ExtendedSession;
|
||||
token: any;
|
||||
}) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
},
|
||||
});
|
||||
65
app/(auth)/login/page.tsx
Normal file
65
app/(auth)/login/page.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useActionState, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { AuthForm } from "@/components/custom/auth-form";
|
||||
import { SubmitButton } from "@/components/custom/submit-button";
|
||||
|
||||
import { login, LoginActionState } from "../actions";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
const [state, formAction] = useActionState<LoginActionState, FormData>(
|
||||
login,
|
||||
{
|
||||
status: "idle",
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === "failed") {
|
||||
toast.error("Invalid credentials!");
|
||||
} else if (state.status === "invalid_data") {
|
||||
toast.error("Failed validating your submission!");
|
||||
} else if (state.status === "success") {
|
||||
router.refresh();
|
||||
}
|
||||
}, [state.status, router]);
|
||||
|
||||
const handleSubmit = (formData: FormData) => {
|
||||
setEmail(formData.get("email") as string);
|
||||
formAction(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
||||
<div className="w-full max-w-md overflow-hidden rounded-2xl flex flex-col gap-12">
|
||||
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
|
||||
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign In</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-zinc-400">
|
||||
Use your email and password to sign in
|
||||
</p>
|
||||
</div>
|
||||
<AuthForm action={handleSubmit} defaultEmail={email}>
|
||||
<SubmitButton>Sign in</SubmitButton>
|
||||
<p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
|
||||
{"Don't have an account? "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
{" for free."}
|
||||
</p>
|
||||
</AuthForm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
app/(auth)/register/page.tsx
Normal file
67
app/(auth)/register/page.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useActionState, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { AuthForm } from "@/components/custom/auth-form";
|
||||
import { SubmitButton } from "@/components/custom/submit-button";
|
||||
|
||||
import { register, RegisterActionState } from "../actions";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [state, formAction] = useActionState<RegisterActionState, FormData>(
|
||||
register,
|
||||
{
|
||||
status: "idle",
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === "user_exists") {
|
||||
toast.error("Account already exists");
|
||||
} else if (state.status === "failed") {
|
||||
toast.error("Failed to create account");
|
||||
} else if (state.status === "invalid_data") {
|
||||
toast.error("Failed validating your submission!");
|
||||
} else if (state.status === "success") {
|
||||
toast.success("Account created successfully");
|
||||
router.refresh();
|
||||
}
|
||||
}, [state, router]);
|
||||
|
||||
const handleSubmit = (formData: FormData) => {
|
||||
setEmail(formData.get("email") as string);
|
||||
formAction(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-background">
|
||||
<div className="w-full max-w-md overflow-hidden rounded-2xl gap-12 flex flex-col">
|
||||
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16">
|
||||
<h3 className="text-xl font-semibold dark:text-zinc-50">Sign Up</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-zinc-400">
|
||||
Create an account with your email and password
|
||||
</p>
|
||||
</div>
|
||||
<AuthForm action={handleSubmit} defaultEmail={email}>
|
||||
<SubmitButton>Sign Up</SubmitButton>
|
||||
<p className="text-center text-sm text-gray-600 mt-4 dark:text-zinc-400">
|
||||
{"Already have an account? "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
{" instead."}
|
||||
</p>
|
||||
</AuthForm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue