feat(auth): update sign-in and sign-up flow with redirect support

This commit is contained in:
cfngc4594 2025-04-14 21:27:06 +08:00
parent 799ab3ccd1
commit 5465450143
9 changed files with 88 additions and 48 deletions

View File

@ -9,7 +9,7 @@ import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-fo
const saltRounds = 10; const saltRounds = 10;
export async function signInWithCredentials(formData: CredentialsSignInFormValues) { export async function signInWithCredentials(formData: CredentialsSignInFormValues, redirectTo?: string) {
try { try {
// Parse credentials using authSchema for validation // Parse credentials using authSchema for validation
const { email, password } = await authSchema.parseAsync(formData); const { email, password } = await authSchema.parseAsync(formData);
@ -33,7 +33,7 @@ export async function signInWithCredentials(formData: CredentialsSignInFormValue
throw new Error("Incorrect password."); throw new Error("Incorrect password.");
} }
await signIn("credentials", { ...formData, redirect: false }); await signIn("credentials", { ...formData, redirectTo, redirect: !!redirectTo });
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
return { error: error instanceof Error ? error.message : "Failed to sign in. Please try again." }; return { error: error instanceof Error ? error.message : "Failed to sign in. Please try again." };
@ -67,3 +67,7 @@ export async function signUpWithCredentials(formData: CredentialsSignUpFormValue
return { error: error instanceof Error ? error.message : "Registration failed. Please try again." }; return { error: error instanceof Error ? error.message : "Registration failed. Please try again." };
} }
} }
export async function signInWithGithub(redirectTo?: string) {
await signIn("github", { redirectTo, redirect: !!redirectTo });
}

View File

@ -1,8 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { auth } from "@/lib/auth"; import { CodeIcon } from "lucide-react";
import { Code } from "lucide-react";
import { redirect } from "next/navigation";
interface AuthLayoutProps { interface AuthLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -11,16 +9,13 @@ interface AuthLayoutProps {
export default async function AuthLayout({ export default async function AuthLayout({
children children
}: AuthLayoutProps) { }: AuthLayoutProps) {
const session = await auth();
if (session) redirect("/");
return ( return (
<div className="grid min-h-svh lg:grid-cols-2"> <div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10"> <div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start"> <div className="flex justify-center gap-2 md:justify-start">
<Link href="/" className="flex items-center gap-2 font-medium"> <Link href="/" className="flex items-center gap-2 font-medium">
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground"> <div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<Code className="size-4" /> <CodeIcon className="size-4" />
</div> </div>
Judge4c Judge4c
</Link> </Link>

View File

@ -1,11 +1,4 @@
import { import { LogOut } from "lucide-react";
LogIn,
LogOut,
} from "lucide-react";
import {
Avatar,
AvatarImage,
} from "@/components/ui/avatar";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -16,8 +9,9 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { auth, signOut } from "@/lib/auth"; import { auth, signOut } from "@/lib/auth";
import { redirect } from "next/navigation";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import LogInButton from "@/components/log-in-button";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { SettingsButton } from "@/components/settings-button"; import { SettingsButton } from "@/components/settings-button";
const UserAvatar = ({ image, name }: { image: string; name: string }) => ( const UserAvatar = ({ image, name }: { image: string; name: string }) => (
@ -27,12 +21,7 @@ const UserAvatar = ({ image, name }: { image: string; name: string }) => (
</Avatar> </Avatar>
); );
async function handleSignIn() { async function handleLogOut() {
"use server";
redirect("/sign-in");
}
async function handleSignOut() {
"use server"; "use server";
await signOut(); await signOut();
} }
@ -57,10 +46,7 @@ export async function AvatarButton() {
{!isLoggedIn ? ( {!isLoggedIn ? (
<DropdownMenuGroup> <DropdownMenuGroup>
<SettingsButton /> <SettingsButton />
<DropdownMenuItem onClick={handleSignIn}> <LogInButton />
<LogIn />
Log In
</DropdownMenuItem>
</DropdownMenuGroup> </DropdownMenuGroup>
) : ( ) : (
<> <>
@ -76,7 +62,7 @@ export async function AvatarButton() {
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<SettingsButton /> <SettingsButton />
<DropdownMenuItem onClick={handleSignOut}> <DropdownMenuItem onClick={handleLogOut}>
<LogOut /> <LogOut />
Log out Log out
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -12,18 +12,20 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { authSchema } from "@/lib/zod"; import { authSchema } from "@/lib/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { signInWithCredentials } from "@/actions/auth"; import { signInWithCredentials } from "@/actions/auth";
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react"; import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
export type CredentialsSignInFormValues = z.infer<typeof authSchema>; export type CredentialsSignInFormValues = z.infer<typeof authSchema>;
export function CredentialsSignInForm() { export function CredentialsSignInForm() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo");
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@ -47,7 +49,7 @@ export function CredentialsSignInForm() {
}); });
} else { } else {
toast.success("Signed In Successfully"); toast.success("Signed In Successfully");
router.push("/"); router.push(redirectTo || "/");
} }
}); });
}; };

View File

@ -12,18 +12,20 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { authSchema } from "@/lib/zod"; import { authSchema } from "@/lib/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { signUpWithCredentials } from "@/actions/auth"; import { signUpWithCredentials } from "@/actions/auth";
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react"; import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
export type CredentialsSignUpFormValues = z.infer<typeof authSchema>; export type CredentialsSignUpFormValues = z.infer<typeof authSchema>;
export function CredentialsSignUpForm() { export function CredentialsSignUpForm() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo");
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
@ -48,11 +50,8 @@ export function CredentialsSignUpForm() {
} else { } else {
toast.success("Account Created", { toast.success("Account Created", {
description: "You can now sign in with your credentials", description: "You can now sign in with your credentials",
action: {
label: "Go to Sign In",
onClick: () => router.push("/sign-in"),
},
}); });
router.push(`/sign-in?${redirectTo}`)
} }
}); });
}; };

View File

@ -1,15 +1,17 @@
import { signIn } from "@/lib/auth"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { signInWithGithub } from "@/actions/auth";
import { useSearchParams } from "next/navigation";
export function GithubSignInForm() { export function GithubSignInForm() {
const searchParams = useSearchParams();
const redirectTo = searchParams.get("redirectTo");
const signInAction = signInWithGithub.bind(null, redirectTo || "/");
return ( return (
<form <form action={signInAction}>
action={async () => { <Button variant="outline" className="w-full" type="submit">
"use server";
await signIn("github");
}}
>
<Button variant="outline" className="w-full">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path <path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"

View File

@ -0,0 +1,24 @@
"use client";
import { LogIn } from "lucide-react";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export default function LogInButton() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const handleLogIn = () => {
const params = new URLSearchParams(searchParams.toString());
params.set("redirectTo", pathname);
router.push(`/sign-in?${params.toString()}`);
};
return (
<DropdownMenuItem onClick={handleLogIn}>
<LogIn className="mr-2 h-4 w-4" />
Log In
</DropdownMenuItem>
);
}

View File

@ -1,7 +1,18 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { GithubSignInForm } from "@/components/github-sign-in-form"; import { GithubSignInForm } from "@/components/github-sign-in-form";
import { CredentialsSignInForm } from "@/components/credentials-sign-in-form"; import { CredentialsSignInForm } from "@/components/credentials-sign-in-form";
export function SignInForm() { export function SignInForm() {
const router = useRouter();
const searchParams = useSearchParams();
const handleSignUp = () => {
const params = new URLSearchParams(searchParams.toString());
router.push(`/sign-up?${params.toString()}`);
};
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2 text-center"> <div className="flex flex-col items-center gap-2 text-center">
@ -19,9 +30,12 @@ export function SignInForm() {
<GithubSignInForm /> <GithubSignInForm />
<div className="text-center text-sm"> <div className="text-center text-sm">
Don&apos;t have an account?{" "} Don&apos;t have an account?{" "}
<a href="/sign-up" className="underline underline-offset-4"> <button
onClick={handleSignUp}
className="underline underline-offset-4"
>
Sign up Sign up
</a> </button>
</div> </div>
</div> </div>
); );

View File

@ -1,7 +1,18 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { GithubSignInForm } from "@/components/github-sign-in-form"; import { GithubSignInForm } from "@/components/github-sign-in-form";
import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form"; import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form";
export function SignUpForm() { export function SignUpForm() {
const router = useRouter();
const searchParams = useSearchParams();
const handleSignIn = () => {
const params = new URLSearchParams(searchParams.toString());
router.push(`/sign-in?${params.toString()}`);
};
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col items-center gap-2 text-center"> <div className="flex flex-col items-center gap-2 text-center">
@ -19,9 +30,12 @@ export function SignUpForm() {
<GithubSignInForm /> <GithubSignInForm />
<div className="text-center text-sm"> <div className="text-center text-sm">
Already have an account?{" "} Already have an account?{" "}
<a href="/sign-in" className="underline underline-offset-4"> <button
onClick={handleSignIn}
className="underline underline-offset-4"
>
Sign in Sign in
</a> </button>
</div> </div>
</div> </div>
); );