diff --git a/package.json b/package.json
index 512991e0..cf8fff68 100644
--- a/package.json
+++ b/package.json
@@ -16,21 +16,28 @@
"@types/node": "18.13.0",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
+ "axios": "^1.3.5",
"chart.js": "^4.2.1",
"clsx": "^1.2.1",
"daisyui": "^2.50.0",
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
+ "firebase": "9.19.1",
"framer-motion": "^9.0.2",
+ "iron-session": "^6.3.1",
"lodash": "^4.17.21",
"next": "13.1.6",
+ "primeicons": "^6.0.1",
+ "primereact": "^9.2.3",
"react": "18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "18.2.0",
+ "react-firebase-hooks": "^5.1.1",
"react-lineto": "^3.3.0",
"react-player": "^2.12.0",
"react-string-replace": "^1.1.0",
"react-toastify": "^9.1.2",
+ "swr": "^2.1.3",
"typescript": "4.9.5",
"zustand": "^4.3.6"
},
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 766e5740..cabaad95 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -1,4 +1,6 @@
+import axios from "axios";
import Link from "next/link";
+import {useRouter} from "next/router";
interface Props {
profilePicture: string;
@@ -6,8 +8,16 @@ interface Props {
/* eslint-disable @next/next/no-img-element */
export default function Navbar({profilePicture}: Props) {
+ const router = useRouter();
+
+ const logout = async () => {
+ axios.post("/api/logout").finally(() => {
+ router.push("/login");
+ });
+ };
+
return (
-
+
IELTS GPT
@@ -34,7 +44,7 @@ export default function Navbar({profilePicture}: Props) {
Settings
- Logout
+ Logout
diff --git a/src/demo/user.json b/src/demo/user.json
index c0c7a700..095650d0 100644
--- a/src/demo/user.json
+++ b/src/demo/user.json
@@ -1,5 +1,5 @@
{
- "username": "tiago.ribeiro",
+ "email": "tiago.ribeiro@ecrop.dev",
"name": {
"first": "Tiago",
"last": "Ribeiro"
diff --git a/src/firebase/index.ts b/src/firebase/index.ts
new file mode 100644
index 00000000..f971a078
--- /dev/null
+++ b/src/firebase/index.ts
@@ -0,0 +1,14 @@
+import {initializeApp} from "firebase/app";
+import {getFirestore} from "firebase/firestore";
+
+const firebaseConfig = {
+ apiKey: process.env.FIREBASE_PUBLIC_API_KEY || "",
+ authDomain: process.env.FIREBASE_AUTH_DOMAIN || "",
+ projectId: process.env.FIREBASE_PROJECT_ID || "",
+ storageBucket: process.env.FIREBASE_STORAGE_BUCKET || "",
+ messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID || "",
+ appId: process.env.FIREBASE_APP_ID || "",
+ measurementId: process.env.FIREBASE_MEASUREMENT_ID || "",
+};
+
+export const app = initializeApp(firebaseConfig);
diff --git a/src/hooks/useUser.tsx b/src/hooks/useUser.tsx
new file mode 100644
index 00000000..c77f6cce
--- /dev/null
+++ b/src/hooks/useUser.tsx
@@ -0,0 +1,29 @@
+import {useEffect} from "react";
+import Router from "next/router";
+import useSWR from "swr";
+import {User} from "@/interfaces/user";
+import axios from "axios";
+
+const fetcher = (url: string) => axios.get(url).then((res) => res.data);
+
+export default function useUser({redirectTo = "", redirectIfFound = false} = {}) {
+ const {data: user, mutate: mutateUser, isLoading} = useSWR
("/api/user", fetcher);
+
+ useEffect(() => {
+ // if no redirect needed, just return (example: already on /dashboard)
+ // if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
+ console.log(redirectTo, user);
+ if (!redirectTo || !user) return;
+
+ if (
+ // If redirectTo is set, redirect if the user was not found.
+ (redirectTo && !redirectIfFound && !user) ||
+ // If redirectIfFound is also set, redirect if the user was found
+ (redirectIfFound && user)
+ ) {
+ Router.push(redirectTo);
+ }
+ }, [user, redirectIfFound, redirectTo]);
+
+ return {user, mutateUser, isLoading};
+}
diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts
index d6b8b5b5..37c786f9 100644
--- a/src/interfaces/user.ts
+++ b/src/interfaces/user.ts
@@ -1,5 +1,5 @@
export interface User {
- username: string;
+ email: string;
name: Name;
profilePicture: string;
id: string;
diff --git a/src/lib/session.ts b/src/lib/session.ts
new file mode 100644
index 00000000..ce071a1d
--- /dev/null
+++ b/src/lib/session.ts
@@ -0,0 +1,18 @@
+// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
+import type {IronSessionOptions} from "iron-session";
+import {User} from "@/interfaces/user";
+
+export const sessionOptions: IronSessionOptions = {
+ password: process.env.SECRET_COOKIE_PASSWORD as string,
+ cookieName: "eCrop/ielts",
+ cookieOptions: {
+ secure: process.env.NODE_ENV === "production",
+ },
+};
+
+// This is where we specify the typings of req.session.*
+declare module "iron-session" {
+ interface IronSessionData {
+ user?: User | null;
+ }
+}
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 196e3d6b..8b694866 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -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 ;
}
diff --git a/src/pages/api/exam/[module].ts b/src/pages/api/exam/[module].ts
new file mode 100644
index 00000000..79febe03
--- /dev/null
+++ b/src/pages/api/exam/[module].ts
@@ -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(),
+ })),
+ );
+}
diff --git a/src/pages/api/login.ts b/src/pages/api/login.ts
new file mode 100644
index 00000000..58b11fbc
--- /dev/null
+++ b/src/pages/api/login.ts
@@ -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});
+ });
+}
diff --git a/src/pages/api/logout.ts b/src/pages/api/logout.ts
new file mode 100644
index 00000000..ce9314af
--- /dev/null
+++ b/src/pages/api/logout.ts
@@ -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});
+ });
+}
diff --git a/src/pages/api/user.ts b/src/pages/api/user.ts
new file mode 100644
index 00000000..87f9ecbc
--- /dev/null
+++ b/src/pages/api/user.ts
@@ -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});
+ }
+}
diff --git a/src/pages/exam/index.tsx b/src/pages/exam/index.tsx
index 8543352a..11d8076e 100644
--- a/src/pages/exam/index.tsx
+++ b/src/pages/exam/index.tsx
@@ -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([]);
const [moduleIndex, setModuleIndex] = useState(0);
const [exam, setExam] = useState();
@@ -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 => {
+ const examRequest = await axios(`/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 ;
+ return ;
}
if (moduleIndex >= selectedModules.length) {
return (
{
setShowSolutions(true);
@@ -110,7 +136,7 @@ export default function Home() {
-
+
{renderScreen()}
>
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 4b58b2b6..66996b24 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -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 (
<>
@@ -26,7 +48,7 @@ export default function Home() {
-
+