Implemented a simple authentication scheme with Firebase and Iron Session
This commit is contained in:
@@ -16,21 +16,28 @@
|
|||||||
"@types/node": "18.13.0",
|
"@types/node": "18.13.0",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.27",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
|
"axios": "^1.3.5",
|
||||||
"chart.js": "^4.2.1",
|
"chart.js": "^4.2.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"daisyui": "^2.50.0",
|
"daisyui": "^2.50.0",
|
||||||
"eslint": "8.33.0",
|
"eslint": "8.33.0",
|
||||||
"eslint-config-next": "13.1.6",
|
"eslint-config-next": "13.1.6",
|
||||||
|
"firebase": "9.19.1",
|
||||||
"framer-motion": "^9.0.2",
|
"framer-motion": "^9.0.2",
|
||||||
|
"iron-session": "^6.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
|
"primeicons": "^6.0.1",
|
||||||
|
"primereact": "^9.2.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-firebase-hooks": "^5.1.1",
|
||||||
"react-lineto": "^3.3.0",
|
"react-lineto": "^3.3.0",
|
||||||
"react-player": "^2.12.0",
|
"react-player": "^2.12.0",
|
||||||
"react-string-replace": "^1.1.0",
|
"react-string-replace": "^1.1.0",
|
||||||
"react-toastify": "^9.1.2",
|
"react-toastify": "^9.1.2",
|
||||||
|
"swr": "^2.1.3",
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
"zustand": "^4.3.6"
|
"zustand": "^4.3.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import axios from "axios";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import {useRouter} from "next/router";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profilePicture: string;
|
profilePicture: string;
|
||||||
@@ -6,8 +8,16 @@ interface Props {
|
|||||||
|
|
||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
export default function Navbar({profilePicture}: Props) {
|
export default function Navbar({profilePicture}: Props) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
axios.post("/api/logout").finally(() => {
|
||||||
|
router.push("/login");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar bg-neutral-100 drop-shadow-md text-black">
|
<div className="navbar bg-neutral-100 drop-shadow-md text-black z-10">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Link className="btn btn-ghost normal-case text-xl" href="/">
|
<Link className="btn btn-ghost normal-case text-xl" href="/">
|
||||||
IELTS GPT
|
IELTS GPT
|
||||||
@@ -34,7 +44,7 @@ export default function Navbar({profilePicture}: Props) {
|
|||||||
<a>Settings</a>
|
<a>Settings</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a>Logout</a>
|
<a onClick={logout}>Logout</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"username": "tiago.ribeiro",
|
"email": "tiago.ribeiro@ecrop.dev",
|
||||||
"name": {
|
"name": {
|
||||||
"first": "Tiago",
|
"first": "Tiago",
|
||||||
"last": "Ribeiro"
|
"last": "Ribeiro"
|
||||||
|
|||||||
14
src/firebase/index.ts
Normal file
14
src/firebase/index.ts
Normal file
@@ -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);
|
||||||
29
src/hooks/useUser.tsx
Normal file
29
src/hooks/useUser.tsx
Normal file
@@ -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<User>("/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};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
email: string;
|
||||||
name: Name;
|
name: Name;
|
||||||
profilePicture: string;
|
profilePicture: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
18
src/lib/session.ts
Normal file
18
src/lib/session.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@ import "@/styles/globals.css";
|
|||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import type {AppProps} from "next/app";
|
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) {
|
export default function App({Component, pageProps}: AppProps) {
|
||||||
return <Component {...pageProps} />;
|
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 {useEffect, useState} from "react";
|
||||||
import {Module} from "@/interfaces";
|
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 Selection from "@/exams/Selection";
|
||||||
import Reading from "@/exams/Reading";
|
import Reading from "@/exams/Reading";
|
||||||
import {Exam, ListeningExam, ReadingExam, UserSolution, WritingExam} from "@/interfaces/exam";
|
import {Exam, ListeningExam, ReadingExam, UserSolution, WritingExam} from "@/interfaces/exam";
|
||||||
import Listening from "@/exams/Listening";
|
import Listening from "@/exams/Listening";
|
||||||
import Writing from "@/exams/Writing";
|
import Writing from "@/exams/Writing";
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer, toast} from "react-toastify";
|
||||||
import Link from "next/link";
|
|
||||||
import Finish from "@/exams/Finish";
|
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 [selectedModules, setSelectedModules] = useState<Module[]>([]);
|
||||||
const [moduleIndex, setModuleIndex] = useState(0);
|
const [moduleIndex, setModuleIndex] = useState(0);
|
||||||
const [exam, setExam] = useState<Exam>();
|
const [exam, setExam] = useState<Exam>();
|
||||||
@@ -27,21 +43,31 @@ export default function Home() {
|
|||||||
const [showSolutions, setShowSolutions] = useState(false);
|
const [showSolutions, setShowSolutions] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
if (selectedModules.length > 0 && moduleIndex < selectedModules.length) {
|
if (selectedModules.length > 0 && moduleIndex < selectedModules.length) {
|
||||||
const nextExam = getExam(selectedModules[moduleIndex]);
|
const nextExam = await getExam(selectedModules[moduleIndex]);
|
||||||
setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined);
|
setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedModules, moduleIndex]);
|
}, [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) {
|
switch (module) {
|
||||||
case "reading":
|
case "reading":
|
||||||
return JSON_READING as ReadingExam;
|
return newExam.shift() as ReadingExam;
|
||||||
case "listening":
|
case "listening":
|
||||||
return JSON_LISTENING as ListeningExam;
|
return newExam.shift() as ListeningExam;
|
||||||
case "writing":
|
case "writing":
|
||||||
return JSON_WRITING as WritingExam;
|
return newExam.shift() as WritingExam;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -63,13 +89,13 @@ export default function Home() {
|
|||||||
|
|
||||||
const renderScreen = () => {
|
const renderScreen = () => {
|
||||||
if (selectedModules.length === 0) {
|
if (selectedModules.length === 0) {
|
||||||
return <Selection user={JSON_USER} onStart={setSelectedModules} />;
|
return <Selection user={user} onStart={setSelectedModules} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moduleIndex >= selectedModules.length) {
|
if (moduleIndex >= selectedModules.length) {
|
||||||
return (
|
return (
|
||||||
<Finish
|
<Finish
|
||||||
user={JSON_USER}
|
user={user}
|
||||||
modules={selectedModules}
|
modules={selectedModules}
|
||||||
onViewResults={() => {
|
onViewResults={() => {
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
@@ -110,7 +136,7 @@ export default function Home() {
|
|||||||
</Head>
|
</Head>
|
||||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
<main className="w-full h-screen flex flex-col items-center bg-neutral-100 text-black">
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Navbar profilePicture={JSON_USER.profilePicture} />
|
<Navbar profilePicture={user.profilePicture} />
|
||||||
{renderScreen()}
|
{renderScreen()}
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,9 +11,31 @@ import ProfileCard from "@/components/ProfileCard";
|
|||||||
|
|
||||||
// TODO: Remove this import
|
// TODO: Remove this import
|
||||||
import JSON_RESULTS from "@/demo/user_results.json";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -26,7 +48,7 @@ export default function Home() {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<main className="w-full h-screen flex flex-col items-center bg-neutral-100">
|
<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">
|
<div className="w-full h-full p-4 relative">
|
||||||
<Link href="/exam">
|
<Link href="/exam">
|
||||||
<button className={clsx("btn gap-2 top-12 right-12 absolute", infoButtonStyle)}>
|
<button className={clsx("btn gap-2 top-12 right-12 absolute", infoButtonStyle)}>
|
||||||
@@ -36,7 +58,7 @@ export default function Home() {
|
|||||||
</Link>
|
</Link>
|
||||||
<section className="h-full w-full flex items-center p-8 gap-12 justify-center">
|
<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">
|
<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>
|
||||||
<section className="w-1/2 h-full flex items-center justify-center">
|
<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" />
|
<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