I'm working on a Next 14 application where I want to protect certain pages. I implemented client auth using Firebase so currently the authentication logic runs client-side to check if the user is logged in or not, and based on that, either allow access to the page or redirect to the login page. Note that my pages are server components when child is client where I check whether user is logged in or not. Which solutions can I consider to make it better then my current approach.
Current solution:
Auth:
const Auth = ({ type }: { type: AuthType }) => {
const {
register,
handleSubmit,
formState: { isValid },
} = useForm<Inputs>();
const { mutate, isPending, error } = useAuth(type);
const onSubmit: SubmitHandler<Inputs> = (data) => mutate(data);
return (
<div className={styles.authForm}>
<div className={styles.header}>
<h1>{type}</h1>
<p>
{type === "Login"
? "Login to get back to your account"
: "Register to start chatting with people"}
</p>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
{type === "Register" && (
<Field
placeholder="Name"
name="name"
register={register}
rules={{
required: true,
minLength: 2,
maxLength: 25,
}}
isPending={isPending}
/>
)}
<Field
placeholder="Email address"
name="email"
isPending={isPending}
register={register}
rules={{
required: true,
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address",
},
}}
/>
<Field
placeholder="Password"
type="password"
name="password"
register={register}
isPending={isPending}
rules={{
required: true,
}}
/>
{error && <ErrorMessage>{error.message}</ErrorMessage>}
<Button isPending={isPending} isValid={isValid}>
Continue
</Button>
{type === "Login" ? (
<Link href="register">Register</Link>
) : (
<Link href="login">Login</Link>
)}
</form>
</div>
);
};
Service:
class AuthService {
async registerUser(data: Inputs) {
try {
const userCredential = await createUserWithEmailAndPassword(
auth,
data.email,
data.password
);
const user = userCredential.user;
await setDoc(doc(db, "users", user.uid), {
uid: user.uid,
name: data.name,
email: user.email,
});
updateProfile(user, {
displayName: data.name,
});
return user;
} catch (error) {
throw error;
}
}
async loginUser(data: Inputs) {
try {
const userCredential = await signInWithEmailAndPassword(
auth,
data.email,
data.password
);
const user = userCredential.user;
return user;
} catch (error) {
throw error;
}
}
logoutUser(router: AppRouterInstance) {
signOut(auth)
.then(() => router.push("/login"))
.catch((error) => alert(error));
}
}
withAuth:
const withAuth = (Component: React.FC) => {
return (props: any) => {
const { currentUser, loading } = useCurrentUserContext();
const router = useRouter();
useEffect(() => {
if (!loading && !currentUser) {
router.push("/login");
}
}, [currentUser, loading, router]);
if (loading) {
return <div>Loading...</div>;
}
return currentUser ? <Component {...props} /> : null;
};
};
Page:
export const metadata: Metadata = {
title: "Users",
};
const Users = () => {
return (
<main className={styles.usersPage}>
<UsersList />
</main>
);
};
export default Users;
UsersList:
const UsersList = () => {
const { users, isLoading, isError, isSuccess } = useUsers();
return (
<>
{(isLoading || isError) && (
<div className={styles.status}>
<Status isLoading={isLoading} isError={isError} />
</div>
)}
{isSuccess && (
<div className={styles.usersList}>
{users?.map((user) => (
<UserCard key={user.uid} data={user} />
))}
</div>
)}
</>
);
};
export default withAuth(UsersList);