Implemented a simple authentication scheme with Firebase and Iron Session
This commit is contained in:
@@ -2,6 +2,10 @@ import "@/styles/globals.css";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import type {AppProps} from "next/app";
|
||||
|
||||
import "primereact/resources/themes/lara-light-indigo/theme.css";
|
||||
import "primereact/resources/primereact.min.css";
|
||||
import "primeicons/primeicons.css";
|
||||
|
||||
export default function App({Component, pageProps}: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
19
src/pages/api/exam/[module].ts
Normal file
19
src/pages/api/exam/[module].ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {getFirestore, collection, getDocs} from "firebase/firestore";
|
||||
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {module} = req.query as {module: string};
|
||||
|
||||
const snapshot = await getDocs(collection(db, module));
|
||||
|
||||
res.status(200).json(
|
||||
snapshot.docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
38
src/pages/api/login.ts
Normal file
38
src/pages/api/login.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {NextApiRequest, NextApiResponse} from "next";
|
||||
import {getAuth, signInWithEmailAndPassword} from "firebase/auth";
|
||||
import {app} from "@/firebase";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {User} from "@/interfaces/user";
|
||||
import {getFirestore, getDoc, doc} from "firebase/firestore";
|
||||
|
||||
const auth = getAuth(app);
|
||||
const db = getFirestore(app);
|
||||
|
||||
export default withIronSessionApiRoute(login, sessionOptions);
|
||||
|
||||
async function login(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {email, password} = req.body as {email: string; password: string};
|
||||
|
||||
signInWithEmailAndPassword(auth, email, password)
|
||||
.then(async (userCredentials) => {
|
||||
const userId = userCredentials.user.uid;
|
||||
|
||||
const docUser = await getDoc(doc(db, "users", userId));
|
||||
if (!docUser.exists()) {
|
||||
res.status(401).json({error: 401, message: "User does not exist!"});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = docUser.data() as User;
|
||||
|
||||
req.session.user = user;
|
||||
await req.session.save();
|
||||
|
||||
res.status(200).json({user: {...user, id: userId}});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
res.status(401).json({error});
|
||||
});
|
||||
}
|
||||
21
src/pages/api/logout.ts
Normal file
21
src/pages/api/logout.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {NextApiRequest, NextApiResponse} from "next";
|
||||
import {getAuth, signOut} from "firebase/auth";
|
||||
import {app} from "@/firebase";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
|
||||
const auth = getAuth(app);
|
||||
|
||||
export default withIronSessionApiRoute(logout, sessionOptions);
|
||||
|
||||
async function logout(req: NextApiRequest, res: NextApiResponse) {
|
||||
signOut(auth)
|
||||
.then(() => {
|
||||
req.session.destroy();
|
||||
res.status(200).json({ok: true});
|
||||
})
|
||||
.catch(() => {
|
||||
req.session.destroy();
|
||||
res.status(500).json({ok: false});
|
||||
});
|
||||
}
|
||||
27
src/pages/api/user.ts
Normal file
27
src/pages/api/user.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {app} from "@/firebase";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {getAuth} from "firebase/auth";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {NextApiRequest, NextApiResponse} from "next";
|
||||
|
||||
const auth = getAuth(app);
|
||||
export default withIronSessionApiRoute(user, sessionOptions);
|
||||
|
||||
async function user(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.session.user) {
|
||||
console.log(auth.currentUser);
|
||||
if (!auth.currentUser) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.session.user.id === auth.currentUser.uid) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({user: req.session.user});
|
||||
} else {
|
||||
res.status(401).json({ok: false});
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,38 @@ import Navbar from "@/components/Navbar";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Module} from "@/interfaces";
|
||||
|
||||
// TODO: Remove this import
|
||||
import JSON_USER from "@/demo/user.json";
|
||||
import JSON_READING from "@/demo/reading.json";
|
||||
import JSON_LISTENING from "@/demo/listening.json";
|
||||
import JSON_WRITING from "@/demo/writing.json";
|
||||
|
||||
import Selection from "@/exams/Selection";
|
||||
import Reading from "@/exams/Reading";
|
||||
import {Exam, ListeningExam, ReadingExam, UserSolution, WritingExam} from "@/interfaces/exam";
|
||||
import Listening from "@/exams/Listening";
|
||||
import Writing from "@/exams/Writing";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
import Link from "next/link";
|
||||
import {ToastContainer, toast} from "react-toastify";
|
||||
import Finish from "@/exams/Finish";
|
||||
import axios from "axios";
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {User} from "@/interfaces/user";
|
||||
|
||||
export default function Home() {
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
|
||||
if (!user) {
|
||||
res.setHeader("location", "/login");
|
||||
res.statusCode = 302;
|
||||
res.end();
|
||||
return {
|
||||
props: {
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {user: req.session.user},
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Page({user}: {user: User}) {
|
||||
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||
const [moduleIndex, setModuleIndex] = useState(0);
|
||||
const [exam, setExam] = useState<Exam>();
|
||||
@@ -27,21 +43,31 @@ export default function Home() {
|
||||
const [showSolutions, setShowSolutions] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedModules.length > 0 && moduleIndex < selectedModules.length) {
|
||||
const nextExam = getExam(selectedModules[moduleIndex]);
|
||||
setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined);
|
||||
}
|
||||
(async () => {
|
||||
if (selectedModules.length > 0 && moduleIndex < selectedModules.length) {
|
||||
const nextExam = await getExam(selectedModules[moduleIndex]);
|
||||
setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedModules, moduleIndex]);
|
||||
|
||||
const getExam = (module: Module): Exam | undefined => {
|
||||
const getExam = async (module: Module): Promise<Exam | undefined> => {
|
||||
const examRequest = await axios<Exam[]>(`/api/exam/${module}`);
|
||||
if (examRequest.status !== 200) {
|
||||
toast.error("Something went wrong!");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newExam = examRequest.data;
|
||||
|
||||
switch (module) {
|
||||
case "reading":
|
||||
return JSON_READING as ReadingExam;
|
||||
return newExam.shift() as ReadingExam;
|
||||
case "listening":
|
||||
return JSON_LISTENING as ListeningExam;
|
||||
return newExam.shift() as ListeningExam;
|
||||
case "writing":
|
||||
return JSON_WRITING as WritingExam;
|
||||
return newExam.shift() as WritingExam;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -63,13 +89,13 @@ export default function Home() {
|
||||
|
||||
const renderScreen = () => {
|
||||
if (selectedModules.length === 0) {
|
||||
return <Selection user={JSON_USER} onStart={setSelectedModules} />;
|
||||
return <Selection user={user} onStart={setSelectedModules} />;
|
||||
}
|
||||
|
||||
if (moduleIndex >= selectedModules.length) {
|
||||
return (
|
||||
<Finish
|
||||
user={JSON_USER}
|
||||
user={user}
|
||||
modules={selectedModules}
|
||||
onViewResults={() => {
|
||||
setShowSolutions(true);
|
||||
@@ -110,7 +136,7 @@ export default function Home() {
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
||||
<ToastContainer />
|
||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
||||
<Navbar profilePicture={user.profilePicture} />
|
||||
{renderScreen()}
|
||||
</main>
|
||||
</>
|
||||
|
||||
@@ -11,9 +11,31 @@ import ProfileCard from "@/components/ProfileCard";
|
||||
|
||||
// TODO: Remove this import
|
||||
import JSON_RESULTS from "@/demo/user_results.json";
|
||||
import JSON_USER from "@/demo/user.json";
|
||||
|
||||
export default function Home() {
|
||||
import {withIronSessionSsr} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {User} from "@/interfaces/user";
|
||||
|
||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||
const user = req.session.user;
|
||||
|
||||
if (!user) {
|
||||
res.setHeader("location", "/login");
|
||||
res.statusCode = 302;
|
||||
res.end();
|
||||
return {
|
||||
props: {
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {user: req.session.user},
|
||||
};
|
||||
}, sessionOptions);
|
||||
|
||||
export default function Home({user}: {user: User}) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -26,7 +48,7 @@ export default function Home() {
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100">
|
||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
||||
<Navbar profilePicture={user.profilePicture} />
|
||||
<div className="w-full h-full p-4 relative">
|
||||
<Link href="/exam">
|
||||
<button className={clsx("btn gap-2 top-12 right-12 absolute", infoButtonStyle)}>
|
||||
@@ -36,7 +58,7 @@ export default function Home() {
|
||||
</Link>
|
||||
<section className="h-full w-full flex items-center p-8 gap-12 justify-center">
|
||||
<section className="w-1/2 h-full flex items-center">
|
||||
<ProfileCard user={JSON_USER} className="text-black self-start" />
|
||||
<ProfileCard user={user} className="text-black self-start" />
|
||||
</section>
|
||||
<section className="w-1/2 h-full flex items-center justify-center">
|
||||
<UserResultChart results={JSON_RESULTS} resultKey="total" label="Total exams" className="w-2/3" />
|
||||
|
||||
82
src/pages/login.tsx
Normal file
82
src/pages/login.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import {User} from "@/interfaces/user";
|
||||
import {toast, ToastContainer} from "react-toastify";
|
||||
import axios from "axios";
|
||||
import {FormEvent, useState} from "react";
|
||||
import Head from "next/head";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import {InputText} from "primereact/inputtext";
|
||||
import {Button} from "primereact/button";
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const {mutateUser} = useUser({
|
||||
redirectTo: "/",
|
||||
redirectIfFound: true,
|
||||
});
|
||||
|
||||
const login = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
axios
|
||||
.post<User>("/api/login", {email, password})
|
||||
.then((response) => {
|
||||
toast.success("You have been logged in!", {toastId: "login-successful"});
|
||||
mutateUser(response.data);
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.response.status === 401) {
|
||||
toast.error("Wrong login credentials!", {toastId: "wrong-credentials"});
|
||||
} else {
|
||||
toast.error("Something went wrong!", {toastId: "server-error"});
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login | IELTS GPT</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="w-full h-screen flex flex-col items-center justify-center bg-neutral-100">
|
||||
<ToastContainer />
|
||||
<form className="p-4 rounded-xl bg-white drop-shadow-xl flex flex-col gap-4" onSubmit={login}>
|
||||
<div className="p-inputgroup">
|
||||
<span className="p-inputgroup-addon">
|
||||
<i className="pi pi-inbox"></i>
|
||||
</span>
|
||||
<InputText
|
||||
placeholder="E-mail..."
|
||||
type="email"
|
||||
required
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-inputgroup">
|
||||
<span className="p-inputgroup-addon">
|
||||
<i className="pi pi-star"></i>
|
||||
</span>
|
||||
<InputText
|
||||
placeholder="Password..."
|
||||
type="password"
|
||||
required
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<Button loading={isLoading} iconPos="right" label="Login" icon="pi pi-check" />
|
||||
</form>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user