Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api integration 💀 #3

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions apps/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@react-oauth/google": "^0.12.1",
"@tanstack/react-query": "^4.36.1",
"axios": "^1.7.7",
"jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
Expand Down
9 changes: 3 additions & 6 deletions apps/frontend/src/api/api.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
export const API_PORT = 5000;
export const API_ADDRESS = `http://localhost:${API_PORT}`;

export enum AUTH_ENDPOINTS {
LOGIN = '/login',
LOGIN_GOOGLE = '/google/login',
CURRENT_USER = '/currentUser',
LOGIN = '/auth/login',
LOGIN_GOOGLE = '/auth/google/login',
CURRENT_USER = '/users/me',
}

export interface LoginResponse {
Expand Down
12 changes: 7 additions & 5 deletions apps/frontend/src/api/config/axios.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import Axios from 'axios';
import {getAccessToken, removeAccessToken} from "../../auth/auth.utils.ts";
// CONSTRAINTS MOVE TO ENV
export const API_PORT = 5000;
export const API_ADDRESS = `http://localhost:${API_PORT}`;

const axios = Axios.create();
const axios = Axios.create({
baseURL: API_ADDRESS,
});

axios.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${getAccessToken() ?? ''}`;
Expand All @@ -12,14 +17,11 @@ axios.interceptors.response.use((res) => res, (err) => {
if (err.request.status === 401 && !err.request.responseURL.includes('login')) {
removeAccessToken();
delete axios.defaults.headers.common.Authorization;
window.location.reload();
}

return err;
});

export default axios;

// CONSTRAINTS MOVE TO ENV
export const API_PORT = 5000;
export const API_ADDRESS = `http://localhost:${API_PORT}`;

25 changes: 16 additions & 9 deletions apps/frontend/src/api/useAuthApi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,22 @@ const useAuthApi = () => {
}
);

const useCurrentUser = () =>
useQuery<User>([
"user",
async () => {
const response = await axios.get(AUTH_ENDPOINTS.CURRENT_USER);
return await handleResponse(response);
}
]
);
const useCurrentUser = () => {
return useQuery<User, Error>({
queryKey: ['user'],
queryFn: async () => {
const response = await axios.get<User>(AUTH_ENDPOINTS.CURRENT_USER);
if (response.status === 200 && response.data) {
return response.data;
} else {
throw new Error('Failed to fetch user data');
}
},
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false,
});
};


const handleResponse = (res: AxiosResponse<LoginResponse>): Promise<LoginResponse> => {
return new Promise<never>((resolve, reject) => {
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/auth/Guard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ interface GuardProps {
const Guard: FC<GuardProps> = ({ children }): ReactElement => {
const location = useLocation();
const isLogin = location.pathname === Route.LOGIN;
const { isAuthenticated, setIsAuthenticated } = useAuthStore();
const {isAuthenticated, setIsAuthenticated } = useAuthStore();
const hasCheckedToken = useRef(false);

useEffect(() => {
if (!isAuthenticated && !hasCheckedToken.current) {
hasCheckedToken.current = true; // to prevent loop
const tokenIsValid = verifyToken();

if (tokenIsValid) {
setIsAuthenticated(true);
}
}
}, [isAuthenticated, setIsAuthenticated]);
}, [isAuthenticated]);

if (isLogin && isAuthenticated) {
return <Navigate to={Route.HOME} state={{ from: location }} replace />;
Expand Down
17 changes: 15 additions & 2 deletions apps/frontend/src/auth/auth.utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import {LoginForm, LoginValidationErrors} from "./auth.types.ts";
import { jwtDecode } from 'jwt-decode';

export const verifyToken = (): boolean => {
return false;
const token = getAccessToken();
if (!token) {
return false;
}

try {
const decodedToken = jwtDecode(token);
const currentTimestamp = Math.floor(Date.now() / 1000);
return (decodedToken.exp ?? 0) > currentTimestamp;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return false;
}
}

export const getAccessToken = () => {
Expand All @@ -25,7 +38,7 @@ export const removeAccessToken = () => {

export const loginFormValidator = (form: LoginForm) => {
const errors: LoginValidationErrors = {};
const emailRegex = /^[\w-\.]{1,30}@([\w-]+\.)+[\w-]{2,4}$/g;
const emailRegex = /^[\w-]{1,30}@([\w-]+\.)+[\w-]{2,4}$/g;
if (!form.email) {
errors.email = 'Email is required';
} else if (!emailRegex.test(form.email)) {
Expand Down
10 changes: 8 additions & 2 deletions apps/frontend/src/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import ScrollableContainer from "./scrollablecontainer/ScrollableContainer.tsx";
import Navbar from "./navbar/Navbar.tsx";
import useThemeStore from "../store/theme/theme.store.ts";
import {Route} from "../router/router.types.ts";
import useAuthApi from '../api/useAuthApi.tsx';
import { User } from '../api/api.types.ts';

interface LayoutProps {
children?: ReactElement;
Expand All @@ -21,11 +23,15 @@ const Layout: FC<LayoutProps> = ({children}): ReactElement => {


const { theme } = useThemeStore();


const { useCurrentUser } = useAuthApi();

const user = useCurrentUser()

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return (<main themestyle={theme.style} thememode={theme.mode}>
{!DISABLE_NAVBAR.includes(route as Route) && <Navbar/>}
{!DISABLE_NAVBAR.includes(route as Route) && <Navbar user={user.data as User}/>}
<ScrollableContainer>
{children}
</ScrollableContainer>
Expand Down
10 changes: 6 additions & 4 deletions apps/frontend/src/layout/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { RiMenuFill } from "react-icons/ri";
import { RiCloseFill } from "react-icons/ri";
import { RiCheckboxBlankCircleLine } from "react-icons/ri";
import {Route} from "../../router/router.types.ts";
import useAuthApi from "../../api/useAuthApi.tsx";
import { User } from '../../api/api.types.ts';



Expand Down Expand Up @@ -50,10 +50,12 @@ const initializeMenu = (): Mode => {
else return translateMenuObject(0)!;
};

const Navbar = () => {
const location = window.location;
export interface NavbarProps {
user: User;
}

const user = useAuthApi().useCurrentUser().data
const Navbar = ({user}: NavbarProps) => {
const location = window.location;

const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [mode, setMode] = useState<Mode>(initializeMenu());
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/pages/login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {loginFormValidator} from "../../auth/auth.utils.ts";
import useAuthApi from "../../api/useAuthApi.tsx";
import {useNavigate} from "react-router-dom";
import {toast} from "react-toastify";
import { Route } from '../../router/router.types.ts';

const initialFormState = {
email: '',
Expand Down Expand Up @@ -42,7 +43,7 @@ const Login: FC = (): ReactElement => {
googleQuery.mutateAsync({token: credential})
.then(() => {
toast("Login success", { type: 'success' });
setTimeout(() => { navigate('/home'); }, 2000);
setTimeout(() => { navigate(Route.HOME); }, 2000);
})
.catch(() => {
toast("Login failed", { type: 'error' });
Expand All @@ -63,7 +64,7 @@ const Login: FC = (): ReactElement => {
loginQuery.mutateAsync(form)
.then(() => {
toast("Login success", { type: 'success' });
setTimeout(() => { navigate('/home'); }, 2000);
setTimeout(() => { navigate(Route.HOME); }, 2000);
})
.catch(() => {
toast("Login failed", { type: 'error' });
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/store/auth/auth.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { create } from "zustand";
import { AuthState, AuthStore } from "./auth.store.types.ts";

const initialValues: AuthState = {
isAuthenticated: false,
isAuthenticated: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's intentional 😉

}

const useAuthStore = create<AuthStore>()((set) =>( {
Expand Down
21 changes: 15 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.