From dbd2e32aded2b680d152fec6594d8eaa3914571c Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 2 Oct 2024 01:12:10 +0800 Subject: [PATCH 01/10] feat: setup next auth --- frontend/components/account/Profile.tsx | 20 ++-- frontend/components/account/Settings.tsx | 30 ++--- frontend/components/auth/Login.tsx | 34 ++---- frontend/components/layout/layout.tsx | 42 +------ frontend/components/layout/navbar.tsx | 21 ++-- frontend/package.json | 1 + frontend/pages/_app.tsx | 19 ++-- frontend/pages/api/auth/[...nextauth].ts | 56 ++++++++++ frontend/pages/index.tsx | 10 +- frontend/pages/questions/index.tsx | 10 +- frontend/services/axios-middleware.ts | 15 +-- frontend/services/user-service-api.ts | 1 + frontend/types/next-auth.d.ts | 41 +++++++ package-lock.json | 135 +++++++++++++++++++++++ 14 files changed, 296 insertions(+), 139 deletions(-) create mode 100644 frontend/pages/api/auth/[...nextauth].ts create mode 100644 frontend/types/next-auth.d.ts diff --git a/frontend/components/account/Profile.tsx b/frontend/components/account/Profile.tsx index f7ae821cf4..ebcf1d8156 100644 --- a/frontend/components/account/Profile.tsx +++ b/frontend/components/account/Profile.tsx @@ -2,19 +2,16 @@ import { InputField, OptionsField } from '../customs/custom-input' import validateInput, { initialFormValues } from '@/util/input-validation' import CustomDialogWithButton from '../customs/custom-dialog' -import { toast } from 'sonner' -import { useMemo, useState } from 'react' -import { updateProfile } from '@/services/user-service-api' import { Proficiency } from '@repo/user-types' import React from 'react' +import { toast } from 'sonner' +import { updateProfile } from '@/services/user-service-api' +import { useSession } from 'next-auth/react' +import { useState } from 'react' function Profile() { - const defaultUsername: string = useMemo(() => { - if (typeof window !== 'undefined') { - return sessionStorage.getItem('username') ?? '' - } - return '' - }, []) + const { data: session, update } = useSession() + const defaultUsername = session?.user.username ?? '' const [formValues, setFormValues] = useState({ ...initialFormValues, username: defaultUsername }) const [formErrors, setFormErrors] = useState({ ...initialFormValues, proficiency: '' }) const [isDialogOpen, toggleDialogOpen] = useState(false) @@ -34,18 +31,17 @@ function Profile() { toggleDialogOpen(false) const userData = { username: formValues.username, proficiency: formValues.proficiency } try { - const response = await updateProfile(sessionStorage.getItem('id') ?? '', userData) + const response = await updateProfile(session?.user.id ?? '', userData) if (response) { const proficiency = Object.values(Proficiency).includes(response.proficiency as Proficiency) ? response.proficiency : Proficiency.BEGINNER - - sessionStorage.setItem('username', response.username ?? '') setFormValues({ ...formValues, username: response.username ?? '', proficiency: proficiency, }) + update({ ...session, user: { ...session?.user, username: response.username, role: response.role } }) toast.success('Profile has been updated successfully.') } } catch (error) { diff --git a/frontend/components/account/Settings.tsx b/frontend/components/account/Settings.tsx index 21448c0dd6..242f7704c3 100644 --- a/frontend/components/account/Settings.tsx +++ b/frontend/components/account/Settings.tsx @@ -1,32 +1,24 @@ +import { deleteAccount, updatePasswordRequest } from '@/services/user-service-api' +import { tokenState, userState } from '@/atoms/auth' +import { useMemo, useState } from 'react' import validateInput, { initialFormValues } from '@/util/input-validation' import CustomDialogWithButton from '../customs/custom-dialog' import { InputField } from '../customs/custom-input' +import React from 'react' import { toast } from 'sonner' import usePasswordToggle from '../../hooks/UsePasswordToggle' -import { useMemo, useState } from 'react' -import { deleteAccount, updatePasswordRequest } from '@/services/user-service-api' -import React from 'react' -import { useSetRecoilState } from 'recoil' -import { tokenState, userState } from '@/atoms/auth' import { useRouter } from 'next/router' +import { useSession } from 'next-auth/react' +import { useSetRecoilState } from 'recoil' function Setting() { const route = useRouter() + const { data: session, update } = useSession() const setIsAuth = useSetRecoilState(userState) const setIsValid = useSetRecoilState(tokenState) - const defaultEmail: string = useMemo(() => { - if (typeof window !== 'undefined') { - return sessionStorage.getItem('email') ?? '' - } - return '' - }, []) - const userId: string = useMemo(() => { - if (typeof window !== 'undefined') { - return sessionStorage.getItem('id') ?? '' - } - return '' - }, []) + const defaultEmail = session?.user.email ?? '' + const userId = session?.user.id ?? '' const [passwordInputType, passwordToggleIcon] = usePasswordToggle() const [confirmPasswordInputType, confirmPasswordToggleIcon] = usePasswordToggle() @@ -44,9 +36,10 @@ function Setting() { // Submit handler const handleFormSubmit = async () => { try { - await updatePasswordRequest({ password: formValues.password }, userId) + const response = await updatePasswordRequest({ password: formValues.password }, userId) setIsFormSubmit(true) toggleUpdateDialogOpen(false) + update({ ...session, user: response }) toast.success('Profile has been updated successfully.') setFormValues({ ...initialFormValues, email: defaultEmail }) } catch (error) { @@ -84,7 +77,6 @@ function Setting() { setIsAuth(false) setIsValid(false) toggleDeleteDialogOpen(false) - sessionStorage.clear() route.push('/auth') toast.success('Successfully Delete Account') } diff --git a/frontend/components/auth/Login.tsx b/frontend/components/auth/Login.tsx index 38e0d597f8..d9bbe14dbf 100644 --- a/frontend/components/auth/Login.tsx +++ b/frontend/components/auth/Login.tsx @@ -1,28 +1,20 @@ 'use client' -import { tokenState, userState } from '@/atoms/auth' import validateInput, { initialFormValues } from '@/util/input-validation' import { Button } from '../ui/button' import { InputField } from '../customs/custom-input' import { PasswordReset } from './PasswordReset' import React from 'react' -import { loginRequest } from '@/services/user-service-api' +import { signIn } from 'next-auth/react' import { toast } from 'sonner' import usePasswordToggle from '../../hooks/UsePasswordToggle' -import { useRouter } from 'next/router' -import { useSetRecoilState } from 'recoil' import { useState } from 'react' export default function Login() { const [formValues, setFormValues] = useState({ ...initialFormValues }) const [formErrors, setFormErrors] = useState({ ...initialFormValues, proficiency: '' }) const [passwordInputType, passwordToggleIcon] = usePasswordToggle() - const setIsAuth = useSetRecoilState(userState) - const setIsValid = useSetRecoilState(tokenState) - - const router = useRouter() - const handleFormChange = (e: React.ChangeEvent): void => { const { id, value } = e.target setFormValues({ ...formValues, [id]: value }) @@ -43,24 +35,14 @@ export default function Login() { setFormErrors(errors) if (isValid) { - const requestBody = { - usernameOrEmail: formValues.email, - password: formValues.loginPassword, - } try { - const res = await loginRequest(requestBody) - if (res) { - sessionStorage.setItem('id', res.id) - sessionStorage.setItem('username', res.username) - sessionStorage.setItem('email', res.email) - sessionStorage.setItem('TTL', new Date().toString()) - sessionStorage.setItem('isAuth', 'true') - sessionStorage.setItem('role', res.role) - setIsAuth(true) - setIsValid(true) - router.push('/') - toast.success('Logged in successfully') - } + await signIn('credentials', { + redirect: true, + username: formValues.email, + password: formValues.loginPassword, + callbackUrl: '/', + }) + toast.success('Logged in successfully') } catch (error) { if (error instanceof Error) { toast.error(error.message) diff --git a/frontend/components/layout/layout.tsx b/frontend/components/layout/layout.tsx index 09dc4b5841..3efa2dba60 100644 --- a/frontend/components/layout/layout.tsx +++ b/frontend/components/layout/layout.tsx @@ -1,54 +1,20 @@ 'use client' -import { userState, tokenState } from '@/atoms/auth' import { NavBar } from '@/components/layout/navbar' -import { inter } from '@/styles/fonts' -import { useRouter } from 'next/router' -import { useEffect } from 'react' -import { useRecoilState } from 'recoil' import React from 'react' +import { inter } from '@/styles/fonts' +import { useSession } from 'next-auth/react' export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { - const router = useRouter() - const { pathname } = router - const [isAuth, setIsAuth] = useRecoilState(userState) - const [isValidToken, setIsValidToken] = useRecoilState(tokenState) - - const isValid = () => { - const ttl = new Date(sessionStorage.getItem('TTL') ?? '').getTime() - const currentTime = new Date().getTime() - // Hard Coded Time Out, to change into env later - if (currentTime - ttl <= 3600000) { - return true - } else { - sessionStorage.setItem('isAuth', 'false') - setIsAuth(false) - setIsValidToken(false) - return false - } - } - - useEffect(() => { - setIsAuth(Boolean(sessionStorage.getItem('isAuth'))) - if (isValid()) { - setIsValidToken(true) - if (pathname === '/auth') { - router.push('/') - } - } else { - setIsValidToken(false) - setIsAuth(false) - router.push('/auth') - } - }, [setIsAuth, setIsValidToken]) + const { data: session } = useSession() return ( <> - {isAuth && isValidToken ? ( + {session ? ( <>
{children}
diff --git a/frontend/components/layout/navbar.tsx b/frontend/components/layout/navbar.tsx index 33cd8937f3..57c42e48c4 100644 --- a/frontend/components/layout/navbar.tsx +++ b/frontend/components/layout/navbar.tsx @@ -13,12 +13,9 @@ import { import Image from 'next/image' import Link from 'next/link' import { LogOutIcon } from 'lucide-react' -import { useSetRecoilState } from 'recoil' -import { tokenState, userState } from '@/atoms/auth' +import { signOut } from 'next-auth/react' export function NavBar() { - const setIsAuth = useSetRecoilState(userState) - const setIsValid = useSetRecoilState(tokenState) return (
@@ -50,18 +47,14 @@ export function NavBar() { - { - setIsAuth(false) - setIsValid(false) - sessionStorage.clear() +
{ + await signOut({ callbackUrl: '/auth' }) }} > -
- -
- + +
) } diff --git a/frontend/package.json b/frontend/package.json index aebdac7f14..952d9325da 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "lucide-react": "^0.441.0", "moment": "^2.30.1", "next": "14.2.11", + "next-auth": "^4.24.8", "react": "^18", "react-ace": "^12.0.0", "react-dom": "^18", diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 69f42d7100..cc2ed08499 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,17 +1,20 @@ import '@/styles/globals.css' import { AppProps } from 'next/app' -import Layout from '@/components/layout/layout' import CustomToaster from '@/components/customs/custom-toaster' +import Layout from '@/components/layout/layout' import { RecoilRoot } from 'recoil' +import { SessionProvider } from 'next-auth/react' -export default function App({ Component, pageProps }: AppProps) { +export default function App({ Component, pageProps: { session, ...pageProps } }: AppProps) { return ( - - - - - - + + + + + + + + ) } diff --git a/frontend/pages/api/auth/[...nextauth].ts b/frontend/pages/api/auth/[...nextauth].ts new file mode 100644 index 0000000000..5b5e2d54fa --- /dev/null +++ b/frontend/pages/api/auth/[...nextauth].ts @@ -0,0 +1,56 @@ +import CredentialsProvider from 'next-auth/providers/credentials' +import NextAuth from 'next-auth' +import axios from 'axios' + +interface Credentials { + username: string + password: string +} + +export default NextAuth({ + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + username: { label: 'Username', type: 'text', placeholder: 'email@site.com' }, + password: { label: 'Password', type: 'password', placeholder: 'password' }, + }, + async authorize(credentials, _req) { + if (!credentials) { + throw new Error('No credentials provided') + } + + const { username, password } = credentials as Credentials + + try { + const api = axios.create({ + baseURL: 'http://localhost:3002', + }) + + const response = await api.post('/auth/login', { usernameOrEmail: username, password }) + + if (!response) { + throw new Error('Invalid username or password') + } + + return response.data + } catch (error) { + throw new Error('Authentication failed') + } + }, + }), + ], + callbacks: { + async jwt({ token, user, trigger, session }) { + if (trigger === 'update') { + return { ...token, ...session.user } + } + return { ...token, ...user } + }, + async session({ session, token }) { + session.user = token as any + return session + }, + }, + secret: process.env.NEXTAUTH_SECRET, +}) diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index ed68051ef3..a9138d7be7 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -3,7 +3,7 @@ import { NewSession } from '@/components/dashboard/new-session' import { ProgressCard } from '@/components/dashboard/progress-card' import { RecentSessions } from '@/components/dashboard/recent-sessions' -import { useEffect, useState } from 'react' +import { useSession } from 'next-auth/react' export default function Home() { const progressData = [ @@ -30,15 +30,11 @@ export default function Home() { }, ] - const [username, setUsername] = useState('') - - useEffect(() => { - setUsername(sessionStorage.getItem('username') ?? '') - }, []) + const { data: session } = useSession() return (
-

Welcome Back, {username}

+

Welcome Back, {session?.user.username}

{progressData.map(({ difficulty, score, progress, indicatorColor, backgroundColor }, index) => ( ([]) const [isLoading, setLoading] = useState(false) const [pagination, setPagination] = useState({ @@ -104,10 +108,6 @@ export default function Questions() { setIsInit(true) }, [isInit]) - useEffect(() => { - setIsAdmin(sessionStorage.getItem('role') === 'ADMIN') - }, []) - const sortHandler = (sortBy: ISortBy) => { setSortBy(sortBy) const body: IGetQuestions = { diff --git a/frontend/services/axios-middleware.ts b/frontend/services/axios-middleware.ts index eedfe6da18..a0c85bb088 100644 --- a/frontend/services/axios-middleware.ts +++ b/frontend/services/axios-middleware.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import { getSession } from 'next-auth/react' const api = axios.create({ baseURL: 'http://localhost:3002', @@ -6,10 +7,10 @@ const api = axios.create({ // Request interceptor for all axios calls api.interceptors.request.use( - (config) => { - const token = sessionStorage.getItem('accessToken') - if (token) { - config.headers['Authorization'] = `Bearer ${token}` + async (config) => { + const session = await getSession() + if (session) { + config.headers['Authorization'] = `Bearer ${session.user.accessToken}` } return config }, @@ -21,12 +22,6 @@ api.interceptors.request.use( // Response interceptor for all axios calls api.interceptors.response.use( (response) => { - const token = response.data?.accessToken - - if (token) { - sessionStorage.setItem('accessToken', token) - } - return response.data }, (error) => { diff --git a/frontend/services/user-service-api.ts b/frontend/services/user-service-api.ts index e2dbf60f21..63898b1c99 100644 --- a/frontend/services/user-service-api.ts +++ b/frontend/services/user-service-api.ts @@ -6,6 +6,7 @@ import { IUserPassword, IUserProfile, } from '@/types/axios-user-types' + import { IEmailVerificationDto } from '@repo/user-types' import { IUserDto } from '@repo/user-types' import axios from 'axios' diff --git a/frontend/types/next-auth.d.ts b/frontend/types/next-auth.d.ts new file mode 100644 index 0000000000..9ed5084d56 --- /dev/null +++ b/frontend/types/next-auth.d.ts @@ -0,0 +1,41 @@ +// next-auth.d.ts +import NextAuth from 'next-auth' +import { JWT } from 'next-auth/jwt' + +declare module 'next-auth' { + // Extend the default `user` type + interface User { + id: string + email: string + username: string + role: string + proficiency: string + accessToken: string + } + + // Extend the default `session` type to include all custom fields + interface Session { + user: { + id: string + email: string + username: string + role: string + proficiency: string + accessToken: string + } + } +} + +declare module 'next-auth/jwt' { + // Extend the default `JWT` type to include custom fields + interface JWT { + data?: { + id: string + email: string + username: string + role: string + proficiency: string + accessToken: string + } + } +} diff --git a/package-lock.json b/package-lock.json index 63a9bc25b5..c148fc2d2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -157,6 +157,7 @@ "lucide-react": "^0.441.0", "moment": "^2.30.1", "next": "14.2.11", + "next-auth": "^4.24.8", "react": "^18", "react-ace": "^12.0.0", "react-dom": "^18", @@ -2312,6 +2313,14 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", @@ -9252,6 +9261,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -10178,6 +10195,45 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.8", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.8.tgz", + "integrity": "sha512-SLt3+8UCtklsotnz2p+nB4aN3IHNmpsQFAZ24VLxGotWGzSxkBh192zxNhm/J5wgkcrDWVp0bwqvW0HksK/Lcw==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.5.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18", + "react-dom": "^17.0.2 || ^18" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-auth/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "funding": [ @@ -10373,6 +10429,11 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -10496,6 +10557,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "license": "MIT", @@ -10534,6 +10603,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -10968,6 +11070,31 @@ "version": "4.2.0", "license": "MIT" }, + "node_modules/preact": { + "version": "10.24.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.1.tgz", + "integrity": "sha512-PnBAwFI3Yjxxcxw75n6VId/5TFxNW/81zexzWD9jn1+eSrOP84NdsS38H5IkF/UH3frqRPT+MvuCoVHjTDTnDw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/preact-render-to-string/node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -13195,6 +13322,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "devOptional": true, From b96fa6027263c98d8a814db60dc9ed747c781a17 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 2 Oct 2024 01:20:18 +0800 Subject: [PATCH 02/10] feat: add protected routes --- frontend/hooks/UseProtectedRoute.tsx | 26 +++++++++++++++++++++++++ frontend/pages/account/index.tsx | 3 +++ frontend/pages/code/index.tsx | 29 +++++++++++++++++----------- frontend/pages/index.tsx | 6 ++++-- frontend/pages/questions/index.tsx | 5 +++++ frontend/pages/sessions/index.tsx | 10 ++++++++-- 6 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 frontend/hooks/UseProtectedRoute.tsx diff --git a/frontend/hooks/UseProtectedRoute.tsx b/frontend/hooks/UseProtectedRoute.tsx new file mode 100644 index 0000000000..ae7e793b0d --- /dev/null +++ b/frontend/hooks/UseProtectedRoute.tsx @@ -0,0 +1,26 @@ +// hooks/useProtectedRoute.ts + +import { useEffect, useState } from 'react' + +import { useRouter } from 'next/router' +import { useSession } from 'next-auth/react' + +const useProtectedRoute = () => { + const { data: session, status } = useSession() + const router = useRouter() + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + if (status === 'loading') return + + if (!session) { + router.push('/auth') + } else { + setIsLoading(false) + } + }, [session, status, router]) + + return { session, loading: isLoading } +} + +export default useProtectedRoute diff --git a/frontend/pages/account/index.tsx b/frontend/pages/account/index.tsx index e951b884fb..9f332690a4 100644 --- a/frontend/pages/account/index.tsx +++ b/frontend/pages/account/index.tsx @@ -1,5 +1,8 @@ import AccountSettings from '@/components/account/AccountSetting' +import useProtectedRoute from '@/hooks/UseProtectedRoute' export default function Account() { + const { loading } = useProtectedRoute() + if (loading) return null return } diff --git a/frontend/pages/code/index.tsx b/frontend/pages/code/index.tsx index 70274fc08d..be47b2dd53 100644 --- a/frontend/pages/code/index.tsx +++ b/frontend/pages/code/index.tsx @@ -1,10 +1,3 @@ -import { DifficultyLabel } from '@/components/customs/difficulty-label' -import { Button } from '@/components/ui/button' -import CustomLabel from '@/components/ui/label' -import Image from 'next/image' -import { useEffect, useRef, useState } from 'react' -import { EndIcon, PlayIcon, SubmitIcon } from '@/assets/icons' -import AceEditor from 'react-ace' import 'ace-builds/src-noconflict/mode-javascript' import 'ace-builds/src-noconflict/mode-python' import 'ace-builds/src-noconflict/mode-java' @@ -14,13 +7,23 @@ import 'ace-builds/src-noconflict/mode-ruby' import 'ace-builds/src-noconflict/mode-typescript' import 'ace-builds/src-noconflict/theme-monokai' import 'ace-builds/src-noconflict/ext-language_tools' -import LanguageModeSelect from './language-mode-select' -import CustomTabs from '@/components/customs/custom-tabs' -import { mockChatData, mockCollaboratorData, mockQuestionData, mockTestCaseData, mockUserData } from '@/mock-data' + +import { EndIcon, PlayIcon, SubmitIcon } from '@/assets/icons' import { IQuestion, ITestcase } from '@/types' +import { mockChatData, mockCollaboratorData, mockQuestionData, mockTestCaseData, mockUserData } from '@/mock-data' +import { useEffect, useRef, useState } from 'react' + +import AceEditor from 'react-ace' +import { Button } from '@/components/ui/button' +import CustomLabel from '@/components/ui/label' +import CustomTabs from '@/components/customs/custom-tabs' +import { DifficultyLabel } from '@/components/customs/difficulty-label' +import Image from 'next/image' +import LanguageModeSelect from './language-mode-select' +import React from 'react' import TestcasesTab from './testcase-tab' +import useProtectedRoute from '@/hooks/UseProtectedRoute' import { useRouter } from 'next/router' -import React from 'react' interface ICollaborator { name: string @@ -94,6 +97,10 @@ export default function Code() { } } + const { loading } = useProtectedRoute() + + if (loading) return null + // Scroll to the bottom of the chat box when new messages are added useEffect(() => { if (chatEndRef.current) { diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index a9138d7be7..daafab1238 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -3,7 +3,7 @@ import { NewSession } from '@/components/dashboard/new-session' import { ProgressCard } from '@/components/dashboard/progress-card' import { RecentSessions } from '@/components/dashboard/recent-sessions' -import { useSession } from 'next-auth/react' +import useProtectedRoute from '@/hooks/UseProtectedRoute' export default function Home() { const progressData = [ @@ -30,7 +30,9 @@ export default function Home() { }, ] - const { data: session } = useSession() + const { session, loading } = useProtectedRoute() + + if (loading) return null return (
diff --git a/frontend/pages/questions/index.tsx b/frontend/pages/questions/index.tsx index 9b02ad28f3..aa66c816f7 100644 --- a/frontend/pages/questions/index.tsx +++ b/frontend/pages/questions/index.tsx @@ -28,6 +28,7 @@ import Datatable from '@/components/customs/datatable' import { Role } from '@repo/user-types' import { capitalizeFirst } from '@/util/string-modification' import { toast } from 'sonner' +import useProtectedRoute from '@/hooks/UseProtectedRoute' import { useSession } from 'next-auth/react' export default function Questions() { @@ -101,6 +102,10 @@ export default function Questions() { const [isInit, setIsInit] = useState(false) + const { loading } = useProtectedRoute() + + if (loading) return null + useEffect(() => { if (!isInit) { loadData() diff --git a/frontend/pages/sessions/index.tsx b/frontend/pages/sessions/index.tsx index 6de4c1159d..8d103da59c 100644 --- a/frontend/pages/sessions/index.tsx +++ b/frontend/pages/sessions/index.tsx @@ -1,8 +1,10 @@ -import Datatable from '@/components/customs/datatable' -import { mockSessionsData } from '@/mock-data' import { IPagination, ISession, ISortBy, SortDirection } from '@/types' import { useEffect, useState } from 'react' + +import Datatable from '@/components/customs/datatable' import { columns } from './columns' +import { mockSessionsData } from '@/mock-data' +import useProtectedRoute from '@/hooks/UseProtectedRoute' export default function Sessions() { const [data, setData] = useState([]) @@ -32,6 +34,10 @@ export default function Sessions() { setSortBy(sortBy) } + const { loading } = useProtectedRoute() + + if (loading) return null + return (

Sessions

From f0a704ddc6200d6e961fbeb6299bf0bbce0d09cb Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 2 Oct 2024 21:23:28 +0800 Subject: [PATCH 03/10] fix: solve auth redirect bug --- frontend/components/auth/Login.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/components/auth/Login.tsx b/frontend/components/auth/Login.tsx index d9bbe14dbf..47b91299a4 100644 --- a/frontend/components/auth/Login.tsx +++ b/frontend/components/auth/Login.tsx @@ -9,6 +9,7 @@ import React from 'react' import { signIn } from 'next-auth/react' import { toast } from 'sonner' import usePasswordToggle from '../../hooks/UsePasswordToggle' +import { useRouter } from 'next/router' import { useState } from 'react' export default function Login() { @@ -20,6 +21,8 @@ export default function Login() { setFormValues({ ...formValues, [id]: value }) } + const router = useRouter() + const onLogin = async () => { const isTest = { email: true, @@ -35,18 +38,17 @@ export default function Login() { setFormErrors(errors) if (isValid) { - try { - await signIn('credentials', { - redirect: true, - username: formValues.email, - password: formValues.loginPassword, - callbackUrl: '/', - }) + const result = await signIn('credentials', { + redirect: false, + username: formValues.email, + password: formValues.loginPassword, + }) + if (result?.error) { + toast.error('Login failed. Please try again') + return + } else { toast.success('Logged in successfully') - } catch (error) { - if (error instanceof Error) { - toast.error(error.message) - } + router.push('/') } } } From 1d476a7d9afa4c117aedb97eb93b19dab9f4aa8b Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 2 Oct 2024 21:30:36 +0800 Subject: [PATCH 04/10] fix: ensure /auth page cant be accessed when logged in --- frontend/pages/auth/index.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/pages/auth/index.tsx b/frontend/pages/auth/index.tsx index f7aceb2971..5d13513a95 100644 --- a/frontend/pages/auth/index.tsx +++ b/frontend/pages/auth/index.tsx @@ -3,10 +3,22 @@ import Image from 'next/image' import Login from '../../components/auth/Login' import Signup from '../../components/auth/Signup' +import { useRouter } from 'next/router' +import { useSession } from 'next-auth/react' import { useState } from 'react' export default function Auth() { const [isLoginPage, setIsLoginPage] = useState(false) + const router = useRouter() + const { data: session, status } = useSession() + + if (status === 'loading') { + return null + } + + if (session) { + router.push('/') + } return (
From b39448e2c0667f43da0696b50a6045ceea6523c0 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 2 Oct 2024 22:10:36 +0800 Subject: [PATCH 05/10] fix: add session expiry --- frontend/pages/_app.tsx | 2 +- frontend/pages/api/auth/[...nextauth].ts | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index cc2ed08499..2c5702620f 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -8,7 +8,7 @@ import { SessionProvider } from 'next-auth/react' export default function App({ Component, pageProps: { session, ...pageProps } }: AppProps) { return ( - + diff --git a/frontend/pages/api/auth/[...nextauth].ts b/frontend/pages/api/auth/[...nextauth].ts index 5b5e2d54fa..beca8ffc3b 100644 --- a/frontend/pages/api/auth/[...nextauth].ts +++ b/frontend/pages/api/auth/[...nextauth].ts @@ -33,7 +33,7 @@ export default NextAuth({ throw new Error('Invalid username or password') } - return response.data + return { ...response.data, accessTokenExpires: Date.now() + 3600 * 1000 } } catch (error) { throw new Error('Authentication failed') } @@ -42,15 +42,31 @@ export default NextAuth({ ], callbacks: { async jwt({ token, user, trigger, session }) { - if (trigger === 'update') { - return { ...token, ...session.user } + if (user) { + return { ...token, ...user } } - return { ...token, ...user } + + if (trigger === 'update' && session?.user) { + return { + ...token, + ...session.user, + } + } + + if (Date.now() > (token.accessTokenExpires as number)) { + return Promise.reject(new Error('Access token has expired')) + } + + return token }, async session({ session, token }) { session.user = token as any return session }, }, + session: { + strategy: 'jwt', + maxAge: 3600, + }, secret: process.env.NEXTAUTH_SECRET, }) From 5d8c0d730e7d765fa6cdd99d65dda1553134722c Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 2 Oct 2024 22:58:25 +0800 Subject: [PATCH 06/10] fix: render edit/delete icon only for admins --- frontend/pages/questions/index.tsx | 15 +-- frontend/pages/questions/props.tsx | 135 +++++++++++++------------ frontend/services/axios-middleware2.ts | 15 +-- 3 files changed, 79 insertions(+), 86 deletions(-) diff --git a/frontend/pages/questions/index.tsx b/frontend/pages/questions/index.tsx index aa66c816f7..71234a6408 100644 --- a/frontend/pages/questions/index.tsx +++ b/frontend/pages/questions/index.tsx @@ -10,7 +10,6 @@ import { QuestionStatus, SortDirection, } from '@/types' -import { columns, formFields } from './props' import { createQuestionRequest, deleteQuestionById, @@ -18,6 +17,7 @@ import { getQuestionsRequest, updateQuestionRequest, } from '@/services/question-service-api' +import { formFields, getColumns } from './props' import { useEffect, useState } from 'react' import { Button } from '@/components/ui/button' @@ -100,19 +100,14 @@ export default function Questions() { setLoading(false) } - const [isInit, setIsInit] = useState(false) + useEffect(() => { + loadData() + }, []) const { loading } = useProtectedRoute() if (loading) return null - useEffect(() => { - if (!isInit) { - loadData() - } - setIsInit(true) - }, [isInit]) - const sortHandler = (sortBy: ISortBy) => { setSortBy(sortBy) const body: IGetQuestions = { @@ -264,7 +259,7 @@ export default function Questions() {
{ - const c = values.map((v: string) => ( - - {v} - - )) - return
{c}
+const getColumns = (isAdmin: boolean): IDatatableColumn[] => { + return [ + { + key: 'id', + isHidden: true, }, - }, - { - key: 'description', - width: '35%', - offAutoCapitalize: true, - formatter: (value) => { - return ( -
- {value} -
- ) + { + key: 'title', + width: '20%', + offAutoCapitalize: true, }, - }, - { - key: 'status', - formatter: (value) => { - return ( -
- {value === QuestionStatus.COMPLETED ? ( - - ) : value === QuestionStatus.FAILED ? ( - - ) : null} -
- ) + { + key: 'categories', + formatter: (values) => { + const c = values.map((v: string) => ( + + {v} + + )) + return
{c}
+ }, }, - }, - { - key: 'complexity', - isSortable: true, - formatter: (value) => { - return + { + key: 'description', + width: '35%', + offAutoCapitalize: true, + formatter: (value) => { + return ( +
+ {value} +
+ ) + }, }, - }, - { - key: 'actions', - isEdit: true, - isDelete: true, - width: '12%', - }, -] + { + key: 'status', + formatter: (value) => { + return ( +
+ {value === QuestionStatus.COMPLETED ? ( + + ) : value === QuestionStatus.FAILED ? ( + + ) : null} +
+ ) + }, + }, + { + key: 'complexity', + isSortable: true, + formatter: (value) => { + return + }, + }, + { + isHidden: !isAdmin, + key: 'actions', + isEdit: true, + isDelete: true, + width: '12%', + }, + ] +} const formFields: IFormFields[] = [ { @@ -113,7 +116,7 @@ const formFields: IFormFields[] = [ }, ] -export { columns, formFields } +export { getColumns, formFields } export default function None() { return null diff --git a/frontend/services/axios-middleware2.ts b/frontend/services/axios-middleware2.ts index 6da50ff402..207b182ae7 100644 --- a/frontend/services/axios-middleware2.ts +++ b/frontend/services/axios-middleware2.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import { getSession } from 'next-auth/react' const api = axios.create({ baseURL: 'http://localhost:3004', @@ -6,10 +7,10 @@ const api = axios.create({ // Request interceptor for all axios calls api.interceptors.request.use( - (config) => { - const token = sessionStorage.getItem('accessToken') - if (token) { - config.headers['Authorization'] = `Bearer ${token}` + async (config) => { + const session = await getSession() + if (session) { + config.headers['Authorization'] = `Bearer ${session.user.accessToken}` } return config }, @@ -21,12 +22,6 @@ api.interceptors.request.use( // Response interceptor for all axios calls api.interceptors.response.use( (response) => { - const token = response.data?.accessToken - - if (token) { - sessionStorage.setItem('accessToken', token) - } - return response.data }, (error) => { From 1b0f0d3645fed80490ad3658c5d879e41bf75395 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 2 Oct 2024 22:59:53 +0800 Subject: [PATCH 07/10] fix: render login page instead of signup page --- frontend/pages/auth/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/auth/index.tsx b/frontend/pages/auth/index.tsx index 5d13513a95..5e7534b43f 100644 --- a/frontend/pages/auth/index.tsx +++ b/frontend/pages/auth/index.tsx @@ -8,7 +8,7 @@ import { useSession } from 'next-auth/react' import { useState } from 'react' export default function Auth() { - const [isLoginPage, setIsLoginPage] = useState(false) + const [isLoginPage, setIsLoginPage] = useState(true) const router = useRouter() const { data: session, status } = useSession() From bcc4ad4ed1b7be179f04c4e3f8c183d2c0ed05a9 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Thu, 3 Oct 2024 00:25:05 +0800 Subject: [PATCH 08/10] fix: add loading page and fix question page bugs --- frontend/components/customs/loading.tsx | 23 +++++++++++++++++++++++ frontend/pages/account/index.tsx | 3 ++- frontend/pages/auth/index.tsx | 3 ++- frontend/pages/index.tsx | 3 ++- frontend/pages/questions/index.tsx | 14 +++++++------- frontend/pages/sessions/index.tsx | 3 ++- frontend/services/question-service-api.ts | 13 ++++++++++--- 7 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 frontend/components/customs/loading.tsx diff --git a/frontend/components/customs/loading.tsx b/frontend/components/customs/loading.tsx new file mode 100644 index 0000000000..7f84762121 --- /dev/null +++ b/frontend/components/customs/loading.tsx @@ -0,0 +1,23 @@ +'use client' + +export default function Loading() { + return ( +
+ + + +
+ ) +} diff --git a/frontend/pages/account/index.tsx b/frontend/pages/account/index.tsx index 9f332690a4..537832078b 100644 --- a/frontend/pages/account/index.tsx +++ b/frontend/pages/account/index.tsx @@ -1,8 +1,9 @@ import AccountSettings from '@/components/account/AccountSetting' +import Loading from '@/components/customs/loading' import useProtectedRoute from '@/hooks/UseProtectedRoute' export default function Account() { const { loading } = useProtectedRoute() - if (loading) return null + if (loading) return return } diff --git a/frontend/pages/auth/index.tsx b/frontend/pages/auth/index.tsx index 5e7534b43f..1e51b6191d 100644 --- a/frontend/pages/auth/index.tsx +++ b/frontend/pages/auth/index.tsx @@ -1,6 +1,7 @@ 'use client' import Image from 'next/image' +import Loading from '@/components/customs/loading' import Login from '../../components/auth/Login' import Signup from '../../components/auth/Signup' import { useRouter } from 'next/router' @@ -13,7 +14,7 @@ export default function Auth() { const { data: session, status } = useSession() if (status === 'loading') { - return null + return } if (session) { diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index daafab1238..40d827d8bf 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,5 +1,6 @@ 'use client' +import Loading from '@/components/customs/loading' import { NewSession } from '@/components/dashboard/new-session' import { ProgressCard } from '@/components/dashboard/progress-card' import { RecentSessions } from '@/components/dashboard/recent-sessions' @@ -32,7 +33,7 @@ export default function Home() { const { session, loading } = useProtectedRoute() - if (loading) return null + if (loading) return return (
diff --git a/frontend/pages/questions/index.tsx b/frontend/pages/questions/index.tsx index 71234a6408..34ead3439b 100644 --- a/frontend/pages/questions/index.tsx +++ b/frontend/pages/questions/index.tsx @@ -25,6 +25,7 @@ import ConfirmDialog from '@/components/customs/confirm-dialog' import CustomForm from '@/components/customs/custom-form' import CustomModal from '@/components/customs/custom-modal' import Datatable from '@/components/customs/datatable' +import Loading from '@/components/customs/loading' import { Role } from '@repo/user-types' import { capitalizeFirst } from '@/util/string-modification' import { toast } from 'sonner' @@ -36,7 +37,6 @@ export default function Questions() { const isAdmin = session?.user.role === Role.ADMIN const [data, setData] = useState([]) - const [isLoading, setLoading] = useState(false) const [pagination, setPagination] = useState({ totalPages: 1, currentPage: 1, @@ -90,24 +90,19 @@ export default function Questions() { } const loadData = async () => { - setLoading(true) const body: IGetQuestions = { page: pagination.currentPage, limit: pagination.limit, sortBy: sortBy, } await load(body) - setLoading(false) } useEffect(() => { + if (!session) return loadData() }, []) - const { loading } = useProtectedRoute() - - if (loading) return null - const sortHandler = (sortBy: ISortBy) => { setSortBy(sortBy) const body: IGetQuestions = { @@ -237,6 +232,11 @@ export default function Questions() { loadData() } + const { loading } = useProtectedRoute() + + if (loading) return + if (data.length === 0) return null + return (
diff --git a/frontend/pages/sessions/index.tsx b/frontend/pages/sessions/index.tsx index 8d103da59c..b39db7946b 100644 --- a/frontend/pages/sessions/index.tsx +++ b/frontend/pages/sessions/index.tsx @@ -2,6 +2,7 @@ import { IPagination, ISession, ISortBy, SortDirection } from '@/types' import { useEffect, useState } from 'react' import Datatable from '@/components/customs/datatable' +import Loading from '@/components/customs/loading' import { columns } from './columns' import { mockSessionsData } from '@/mock-data' import useProtectedRoute from '@/hooks/UseProtectedRoute' @@ -36,7 +37,7 @@ export default function Sessions() { const { loading } = useProtectedRoute() - if (loading) return null + if (loading) return return (
diff --git a/frontend/services/question-service-api.ts b/frontend/services/question-service-api.ts index 1a5915036f..3b562f44bd 100644 --- a/frontend/services/question-service-api.ts +++ b/frontend/services/question-service-api.ts @@ -1,4 +1,5 @@ import { IGetQuestions, IGetQuestionsDto, IQuestion, IQuestionsApi, SortDirection } from '@/types' + import axios from 'axios' import axiosInstance from './axios-middleware2' @@ -65,7 +66,9 @@ export const createQuestionRequest = async (data: IQuestion): Promise Date: Thu, 3 Oct 2024 00:45:38 +0800 Subject: [PATCH 09/10] fix: clean up axios middleware code --- frontend/pages/questions/index.tsx | 2 +- frontend/services/axios-middleware.ts | 49 +++++++++++++++++++---- frontend/services/axios-middleware2.ts | 41 ------------------- frontend/services/question-service-api.ts | 4 +- frontend/services/user-service-api.ts | 4 +- 5 files changed, 48 insertions(+), 52 deletions(-) delete mode 100644 frontend/services/axios-middleware2.ts diff --git a/frontend/pages/questions/index.tsx b/frontend/pages/questions/index.tsx index 34ead3439b..a056753412 100644 --- a/frontend/pages/questions/index.tsx +++ b/frontend/pages/questions/index.tsx @@ -235,7 +235,7 @@ export default function Questions() { const { loading } = useProtectedRoute() if (loading) return - if (data.length === 0) return null + if (!data || data?.length === 0) return null return (
diff --git a/frontend/services/axios-middleware.ts b/frontend/services/axios-middleware.ts index a0c85bb088..5ddd991a4e 100644 --- a/frontend/services/axios-middleware.ts +++ b/frontend/services/axios-middleware.ts @@ -1,12 +1,46 @@ import axios from 'axios' import { getSession } from 'next-auth/react' -const api = axios.create({ - baseURL: 'http://localhost:3002', +const userServiceAPI = axios.create({ + baseURL: process.env.NEXT_PUBLIC_USER_SERVICE_URL, }) -// Request interceptor for all axios calls -api.interceptors.request.use( +const questionServiceAPI = axios.create({ + baseURL: process.env.NEXT_PUBLIC_QUESTION_SERVICE_URL, +}) + +userServiceAPI.interceptors.request.use( + async (config) => { + const session = await getSession() + if (session) { + config.headers['Authorization'] = `Bearer ${session.user.accessToken}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +userServiceAPI.interceptors.response.use( + (response) => { + return response.data + }, + (error) => { + if (error.response) { + switch (error.response.status) { + case 403: + console.error('Access denied. You do not have permission to access this resource.') + break + case 500: + throw new Error('Failed to connect to server, please try again!') + } + } + return Promise.reject(error) + } +) + +questionServiceAPI.interceptors.request.use( async (config) => { const session = await getSession() if (session) { @@ -19,15 +53,14 @@ api.interceptors.request.use( } ) -// Response interceptor for all axios calls -api.interceptors.response.use( +questionServiceAPI.interceptors.response.use( (response) => { return response.data }, (error) => { if (error.response) { switch (error.response.status) { - case 403: // Forbidden + case 403: console.error('Access denied. You do not have permission to access this resource.') break case 500: @@ -38,4 +71,4 @@ api.interceptors.response.use( } ) -export default api +export default { userServiceAPI, questionServiceAPI } diff --git a/frontend/services/axios-middleware2.ts b/frontend/services/axios-middleware2.ts deleted file mode 100644 index 207b182ae7..0000000000 --- a/frontend/services/axios-middleware2.ts +++ /dev/null @@ -1,41 +0,0 @@ -import axios from 'axios' -import { getSession } from 'next-auth/react' - -const api = axios.create({ - baseURL: 'http://localhost:3004', -}) - -// Request interceptor for all axios calls -api.interceptors.request.use( - async (config) => { - const session = await getSession() - if (session) { - config.headers['Authorization'] = `Bearer ${session.user.accessToken}` - } - return config - }, - (error) => { - return Promise.reject(error) - } -) - -// Response interceptor for all axios calls -api.interceptors.response.use( - (response) => { - return response.data - }, - (error) => { - if (error.response) { - switch (error.response.status) { - case 403: - console.error('Access denied. You do not have permission to access this resource.') - break - case 500: - throw new Error('Failed to connect to server, please try again!') - } - } - return Promise.reject(error) - } -) - -export default api diff --git a/frontend/services/question-service-api.ts b/frontend/services/question-service-api.ts index 3b562f44bd..da395b06dc 100644 --- a/frontend/services/question-service-api.ts +++ b/frontend/services/question-service-api.ts @@ -1,7 +1,9 @@ import { IGetQuestions, IGetQuestionsDto, IQuestion, IQuestionsApi, SortDirection } from '@/types' import axios from 'axios' -import axiosInstance from './axios-middleware2' +import axiosClient from './axios-middleware' + +const axiosInstance = axiosClient.questionServiceAPI // GET /questions export const getQuestionsRequest = async (data: IGetQuestions): Promise => { diff --git a/frontend/services/user-service-api.ts b/frontend/services/user-service-api.ts index 63898b1c99..01172b3d72 100644 --- a/frontend/services/user-service-api.ts +++ b/frontend/services/user-service-api.ts @@ -10,7 +10,9 @@ import { import { IEmailVerificationDto } from '@repo/user-types' import { IUserDto } from '@repo/user-types' import axios from 'axios' -import axiosInstance from './axios-middleware' +import axiosClient from './axios-middleware' + +const axiosInstance = axiosClient.userServiceAPI // GET /validate export const validateToken = async (): Promise => { From 7c165b1ed71f026a718f8c58d96f0331a693152a Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Fri, 4 Oct 2024 13:00:14 +0800 Subject: [PATCH 10/10] fix: resolve ci --- frontend/components/ui/label.tsx | 7 +++++-- frontend/pages/code/index.tsx | 9 +++++---- frontend/pages/questions/props.tsx | 6 ++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx index b9fed50d2e..7293eb39fd 100644 --- a/frontend/components/ui/label.tsx +++ b/frontend/components/ui/label.tsx @@ -2,11 +2,14 @@ interface CustomLabelProps { title: string textColor: string bgColor: string + margin?: string } -export default function CustomLabel({ title, textColor, bgColor }: CustomLabelProps) { +export default function CustomLabel({ title, textColor, bgColor, margin }: CustomLabelProps) { return ( - + {title} ) diff --git a/frontend/pages/code/index.tsx b/frontend/pages/code/index.tsx index be47b2dd53..49ec91ebfa 100644 --- a/frontend/pages/code/index.tsx +++ b/frontend/pages/code/index.tsx @@ -1,3 +1,4 @@ +import 'ace-builds/src-noconflict/ace' import 'ace-builds/src-noconflict/mode-javascript' import 'ace-builds/src-noconflict/mode-python' import 'ace-builds/src-noconflict/mode-java' @@ -97,10 +98,6 @@ export default function Code() { } } - const { loading } = useProtectedRoute() - - if (loading) return null - // Scroll to the bottom of the chat box when new messages are added useEffect(() => { if (chatEndRef.current) { @@ -130,6 +127,10 @@ export default function Code() { router.push('/') } + const { loading } = useProtectedRoute() + + if (loading) return null + return (
diff --git a/frontend/pages/questions/props.tsx b/frontend/pages/questions/props.tsx index ebf001593c..4594d4bbe7 100644 --- a/frontend/pages/questions/props.tsx +++ b/frontend/pages/questions/props.tsx @@ -1,8 +1,8 @@ import { Difficulty, FormType, IDatatableColumn, IFormFields, QuestionStatus } from '@/types' import { ExclamationIcon, TickIcon } from '@/assets/icons' -import { Badge } from '@/components/ui/badge' import { Category } from '@repo/user-types' +import CustomLabel from '@/components/ui/label' import { DifficultyLabel } from '@/components/customs/difficulty-label' const getColumns = (isAdmin: boolean): IDatatableColumn[] => { @@ -20,9 +20,7 @@ const getColumns = (isAdmin: boolean): IDatatableColumn[] => { key: 'categories', formatter: (values) => { const c = values.map((v: string) => ( - - {v} - + )) return
{c}
},