Add basic authentication and redirection handling

This commit is contained in:
pmoharana-cmd 2024-04-22 10:47:04 -04:00
parent 7dc5aca9ee
commit 755d1523d0
7 changed files with 124 additions and 31 deletions

View File

@ -5,22 +5,34 @@ import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server"; import { createClient } from "@/utils/supabase/server";
export async function login(username: string, password: string) { const supabase = createClient();
const supabase = createClient();
export async function login(email: string, password: string) {
// type-casting here for convenience // type-casting here for convenience
// in practice, you should validate your inputs // in practice, you should validate your inputs
const data = { const data = {
email: username, email,
password: password, password,
}; };
const { error } = await supabase.auth.signInWithPassword(data); const { error } = await supabase.auth.signInWithPassword(data);
if (error) { if (error) {
redirect("/auth/error"); return "Incorrect email/password";
} }
revalidatePath("/resource", "layout"); revalidatePath("/resource", "layout");
redirect("/resource"); redirect("/resource");
} }
export async function signOut() {
const { data, error } = await supabase.auth.getUser();
if (error || !data?.user) {
redirect("auth/login");
}
supabase.auth.signOut();
revalidatePath("/resource", "layout");
redirect("/auth/login");
}

View File

@ -5,18 +5,39 @@ import Button from "@/components/Button";
import Input from "@/components/Input"; import Input from "@/components/Input";
import InlineLink from "@/components/InlineLink"; import InlineLink from "@/components/InlineLink";
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useEffect, useState } from "react";
import PasswordInput from "@/components/auth/PasswordInput"; import PasswordInput from "@/components/auth/PasswordInput";
import ErrorBanner from "@/components/auth/ErrorBanner"; import ErrorBanner from "@/components/auth/ErrorBanner";
import { login } from "../actions";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
export default function Page() { export default function Page() {
const router = useRouter();
useEffect(() => {
const supabase = createClient();
async function checkUser() {
const { data } = await supabase.auth.getUser();
if (data.user) {
router.push("/resource");
}
}
checkUser();
}, [router]);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState(""); const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState(""); const [passwordError, setPasswordError] = useState("");
const [loginError, setLoginError] = useState("");
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.currentTarget.value); setEmail(event.currentTarget.value);
setEmail;
}; };
const handlePasswordChange = ( const handlePasswordChange = (
@ -25,23 +46,33 @@ export default function Page() {
setPassword(event.currentTarget.value); setPassword(event.currentTarget.value);
}; };
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
// Priority: Incorrect combo > Missing email > Missing password event.preventDefault();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (email.trim().length === 0) {
console.log(email);
setEmailError("Please enter your email.");
return;
}
if (!emailRegex.test(email)) {
setEmailError("Please enter a valid email address.");
return;
}
setEmailError("");
if (password.trim().length === 0) { if (password.trim().length === 0) {
setEmailError("Please enter your password."); console.log(password);
event.preventDefault(); setPasswordError("Please enter your password.");
} return;
// This shouldn't happen, <input type="email"> already provides validation, but just in case.
if (email.trim().length === 0) {
setPasswordError("Please enter your email.");
event.preventDefault();
}
// Placeholder for incorrect email + password combo.
if (email === "incorrect@gmail.com" && password) {
setPasswordError("Incorrect password.");
event.preventDefault();
} }
setPasswordError("");
const error = await login(email, password);
setLoginError(error);
}; };
return ( return (
@ -60,7 +91,7 @@ export default function Page() {
type="email" type="email"
valid={emailError == ""} valid={emailError == ""}
title="Email" title="Email"
placeholder="janedoe@gmail.com" placeholder="Enter Email"
onChange={handleEmailChange} onChange={handleEmailChange}
required required
/> />
@ -70,6 +101,7 @@ export default function Page() {
<div className="mb-6"> <div className="mb-6">
<PasswordInput <PasswordInput
title="Password" title="Password"
placeholder="Enter Password"
valid={passwordError == ""} valid={passwordError == ""}
onChange={handlePasswordChange} onChange={handlePasswordChange}
/> />
@ -82,6 +114,8 @@ export default function Page() {
</InlineLink> </InlineLink>
<Button onClick={handleClick}>Login</Button> <Button onClick={handleClick}>Login</Button>
</div> </div>
{loginError && <ErrorBanner heading={loginError} />}
</> </>
); );
} }

View File

@ -7,7 +7,7 @@ import InlineLink from "@/components/InlineLink";
import Paper from "@/components/auth/Paper"; import Paper from "@/components/auth/Paper";
// import { Metadata } from 'next' // import { Metadata } from 'next'
import Image from "next/image"; import Image from "next/image";
import { ChangeEvent, useState } from "react"; import { useState } from "react";
// export const metadata: Metadata = { // export const metadata: Metadata = {
// title: 'Login', // title: 'Login',

View File

@ -3,6 +3,10 @@
import Sidebar from "@/components/resource/Sidebar"; import Sidebar from "@/components/resource/Sidebar";
import React, { useState } from "react"; import React, { useState } from "react";
import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline";
import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react";
import { User } from "@supabase/supabase-js";
import { useRouter } from "next/navigation";
export default function RootLayout({ export default function RootLayout({
children, children,
@ -10,6 +14,26 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [user, setUser] = useState<User>();
const router = useRouter();
useEffect(() => {
const supabase = createClient();
async function getUser() {
const { data, error } = await supabase.auth.getUser();
if (error || data.user === null) {
router.push("auth/login");
return;
}
setUser(data.user);
console.log(data.user);
}
getUser();
}, [router]);
return ( return (
<div className="flex-row"> <div className="flex-row">
@ -27,13 +51,21 @@ export default function RootLayout({
</button> </button>
{/* sidebar */} {/* sidebar */}
<div <div
className={`absolute inset-y-0 left-0 transform ${isSidebarOpen ? "translate-x-0" : "-translate-x-full"} w-64 transition duration-300 ease-in-out`} className={`absolute inset-y-0 left-0 transform ${
isSidebarOpen ? "translate-x-0" : "-translate-x-full"
} w-64 transition duration-300 ease-in-out`}
> >
<Sidebar setIsSidebarOpen={setIsSidebarOpen} /> <Sidebar
name={""}
email={(user && user.email) ?? "No email found!"}
setIsSidebarOpen={setIsSidebarOpen}
/>
</div> </div>
{/* page ui */} {/* page ui */}
<div <div
className={`flex-1 transition duration-300 ease-in-out ${isSidebarOpen ? "ml-64" : "ml-0"}`} className={`flex-1 transition duration-300 ease-in-out ${
isSidebarOpen ? "ml-64" : "ml-0"
}`}
> >
{children} {children}
</div> </div>

View File

@ -11,9 +11,11 @@ import { UserProfile } from "./UserProfile";
interface SidebarProps { interface SidebarProps {
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>; setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
name: string;
email: string;
} }
const Sidebar: React.FC<SidebarProps> = ({ setIsSidebarOpen }) => { const Sidebar: React.FC<SidebarProps> = ({ setIsSidebarOpen, name, email }) => {
return ( return (
<div className="w-64 h-full border border-gray-200 bg-gray-50 px-4"> <div className="w-64 h-full border border-gray-200 bg-gray-50 px-4">
{/* button to close sidebar */} {/* button to close sidebar */}
@ -29,7 +31,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setIsSidebarOpen }) => {
<div className="flex flex-col space-y-8"> <div className="flex flex-col space-y-8">
{/* user + logout button */} {/* user + logout button */}
<div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md "> <div className="flex items-center p-4 space-x-2 border border-gray-200 rounded-md ">
<UserProfile /> <UserProfile name={name} email={email} />
</div> </div>
{/* navigation menu */} {/* navigation menu */}
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">

View File

@ -1,13 +1,23 @@
export const UserProfile = () => { import { signOut } from "@/app/auth/actions";
interface UserProfileProps {
name: string;
email: string;
}
export const UserProfile = ({ name, email }: UserProfileProps) => {
return ( return (
<div className="flex flex-col items-start space-y-2"> <div className="flex flex-col items-start space-y-2">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-semibold text-gray-800"> <span className="text-sm font-semibold text-gray-800">
Compass Center {name}
</span> </span>
<span className="text-xs text-gray-500">cssgunc@gmail.com</span> <span className="text-xs text-gray-500">{email}</span>
</div> </div>
<button className="text-red-600 font-semibold text-xs hover:underline mt-1"> <button
onClick={signOut}
className="text-red-600 font-semibold text-xs hover:underline mt-1"
>
Sign out Sign out
</button> </button>
</div> </div>

View File

@ -3,6 +3,9 @@ const nextConfig = {
images: { images: {
domains: ["notioly.com"], domains: ["notioly.com"],
}, },
experimental: {
serverActions: true
}
}; };
module.exports = nextConfig; module.exports = nextConfig;