🔧 fix(authGuard): add missing newline at the end of the file

🆕 feat(headerComponent): add logout functionality and redirect to login page on sign out

🔧 fix(constants): add missing URL to the list of excluded error retries

🔧 fix(authContext): add refreshToken state and update login and logout functions to handle refresh token

🆕 feat(api): add interceptor to handle access token expiration and refresh

🔧 fix(api): add missing request interceptor to add access token to every request

🔧 fix(API/index.ts): import missing LoginAuthType and LoginType from types/api/index to resolve compilation error
 feat(API/index.ts): add onLogin function to handle user login and authentication
 feat(API/index.ts): add renewAccessToken function to handle token renewal
🔧 fix(loginPage/index.tsx): import missing onLogin function from controllers/API to resolve compilation error
 feat(loginPage/index.tsx): add signIn function to handle user sign in and authentication
🔧 fix(routes.tsx): import ProtectedRoute component from components/authGuard to resolve compilation error
 feat(routes.tsx): add protected routes for HomePage, FlowPage, AdminPage, and DeleteAccountPage to enforce authentication
🔧 fix(api/index.ts): add missing grant_type, scope, client_id, and client_secret properties to LoginType to match API requirements
 feat(api/index.ts): add LoginAuthType to represent the authentication response from the server
🔧 fix(contexts/auth.ts): add refreshToken property to AuthContextType to store the refresh token
This commit is contained in:
Cristhian Zanforlin Lousa 2023-08-11 11:33:52 -03:00
commit c0f586ad89
11 changed files with 240 additions and 42 deletions

View file

@ -0,0 +1,14 @@
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "../../contexts/authContext";
export const ProtectedRoute = ({ children }) => {
const { isAuthenticated } = useContext(AuthContext);
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
};

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { Link, useLocation } from "react-router-dom";
import { Link, useLocation, useNavigate } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
@ -11,12 +11,15 @@ import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar";
import { AuthContext } from "../../contexts/authContext";
export default function Header() {
const { flows, tabId } = useContext(TabsContext);
const { dark, setDark } = useContext(darkContext);
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
const { logout } = useContext(AuthContext);
const navigate = useNavigate();
const [stars, setStars] = useState(null);
@ -34,7 +37,13 @@ export default function Header() {
<Link to="/">
<span className="ml-4 text-2xl"></span>
</Link>
<Button variant="outline" className="">
<Button
onClick={() => {
logout();
navigate("/login");
}}
variant="outline" className="">
Sign out
</Button>
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (

View file

@ -507,6 +507,7 @@ export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code",
"/api/v1/custom_component",
"/api/v1/validate/prompt",
"http://localhost:7860/login"
];
export const CONTROL_INPUT_STATE = {
password: "",

View file

@ -1,9 +1,12 @@
import { createContext, useEffect, useState } from "react";
import { AuthContextType, userData } from "../types/contexts/auth";
import { LoginType } from "../types/api";
import { api } from "../controllers/API/api";
const initialValue: AuthContextType = {
isAuthenticated: false,
accessToken: null,
refreshToken: null,
login: () => {},
logout: () => {},
refreshAccessToken: () => Promise.resolve(),
@ -11,10 +14,12 @@ const initialValue: AuthContextType = {
setUserData: () => {},
};
const AuthContext = createContext<AuthContextType>(initialValue);
export const AuthContext = createContext<AuthContextType>(initialValue);
export function AuthProvider({ children }): React.ReactElement {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [userData, setUserData] = useState<userData>(null);
useEffect(() => {
@ -27,18 +32,23 @@ export function AuthProvider({ children }): React.ReactElement {
function login(newAccessToken: string, refreshToken: string) {
localStorage.setItem("access_token", newAccessToken);
setAccessToken(newAccessToken);
// Store refreshToken if needed
localStorage.setItem("refresh_token", refreshToken);
setRefreshToken(refreshToken);
setIsAuthenticated(true);
}
function logout() {
localStorage.removeItem("access_token");
// Clear refreshToken if used
localStorage.removeItem("refresh_token");
setAccessToken(null);
setRefreshToken(null);
setIsAuthenticated(false);
}
async function refreshAccessToken(refreshToken: string) {
try {
// Call your API to refresh the access token using the refresh token
const response = await fetch("/api/refresh-token", {
method: "POST",
headers: {
@ -57,6 +67,7 @@ export function AuthProvider({ children }): React.ReactElement {
logout();
}
}
return (
// !! to convert string to boolean
@ -64,6 +75,7 @@ export function AuthProvider({ children }): React.ReactElement {
value={{
isAuthenticated: !!accessToken,
accessToken,
refreshToken,
login,
logout,
refreshAccessToken,

View file

@ -8,6 +8,7 @@ import { LocationProvider } from "./locationContext";
import { TabsProvider } from "./tabsContext";
import { TypesProvider } from "./typesContext";
import { UndoRedoProvider } from "./undoRedoContext";
import { AuthProvider } from "./authContext";
export default function ContextWrapper({ children }: { children: ReactNode }) {
//element to wrap all context
@ -18,6 +19,7 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
<DarkProvider>
<TypesProvider>
<LocationProvider>
<AuthProvider>
<AlertProvider>
<SSEProvider>
<TabsProvider>
@ -25,6 +27,7 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
</TabsProvider>
</SSEProvider>
</AlertProvider>
</AuthProvider>
</LocationProvider>
</TypesProvider>
</DarkProvider>

View file

@ -1,6 +1,10 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect, useRef } from "react";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { URL_EXCLUDED_FROM_ERROR_RETRIES } from "../../constants/constants";
import { renewAccessToken } from ".";
import { useNavigate } from "react-router-dom";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
@ -10,44 +14,80 @@ const api: AxiosInstance = axios.create({
function ApiInterceptor() {
const retryCounts = useRef([]);
const { setErrorData } = useContext(alertContext);
const { accessToken, refreshAccessToken, login, logout } = useContext(AuthContext);
const navigate = useNavigate();
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
// return Promise.reject(error);
// }
// let retryCount = 0;
// while (retryCount < 4) {
// await sleep(5000); // Sleep for 5 seconds
// retryCount++;
// try {
// const response = await axios.request(error.config);
// return response;
// } catch (error) {
// if (retryCount === 3) {
// setErrorData({
// title: "There was an error on web connection, please: ",
// list: [
// "Refresh the page",
// "Use a new flow tab",
// "Check if the backend is up",
// "Endpoint: " + error.config?.url,
// ],
// });
// return Promise.reject(error);
// }
// }
// }
if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
return Promise.reject(error);
}
if(error.response?.status === 401){
const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
const res = await renewAccessToken(refreshToken);
login(res.access_token, res.refresh_token)
try {
const response = await axios.request(error.config);
return response;
} catch (error) {
if(error.response?.status === 401){
logout();
navigate("/login");
}
}
}
}
else{
let retryCount = 0;
while (retryCount < 4) {
await sleep(5000); // Sleep for 5 seconds
retryCount++;
try {
const response = await axios.request(error.config);
return response;
} catch (error) {
if (retryCount === 3) {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
"Refresh the page",
"Use a new flow tab",
"Check if the backend is up",
"Endpoint: " + error.config?.url,
],
});
return Promise.reject(error);
}
}
}
}
}
);
// Request interceptor to add access token to every request
const requestInterceptor = api.interceptors.request.use(
(config) => {
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
return () => {
// Clean up the interceptor when the component unmounts
// Clean up the interceptors when the component unmounts
api.interceptors.response.eject(interceptor);
api.interceptors.request.eject(requestInterceptor);
};
}, [retryCounts]);
}, [accessToken, setErrorData]);
return null;
}

View file

@ -1,7 +1,7 @@
import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow";
import { api } from "../../controllers/API/api";
import { APIObjectType, sendAllProps } from "../../types/api/index";
import { APIObjectType, LoginAuthType, LoginType, sendAllProps } from "../../types/api/index";
import { FlowStyleType, FlowType } from "../../types/flow";
import {
APIClassType,
@ -346,3 +346,43 @@ export async function postCustomComponent(
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`/api/v1/custom_component`, { code });
}
export async function onLogin(
user: LoginType
) {
try {
const response = await api.post(
"http://localhost:7860/login",
new URLSearchParams({
username: user.username,
password: user.password,
}).toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
if (response.status === 200) {
const data = response.data;
return data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function renewAccessToken(
token: string
): Promise<LoginAuthType> {
try {
return await api.post(`http://localhost:7860/refresh?token=${token}`);
} catch (error) {
console.log("Error:", error);
throw error;
}
}

View file

@ -1,6 +1,6 @@
import * as Form from "@radix-ui/react-form";
import { useState } from "react";
import { Link } from "react-router-dom";
import { useContext, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Button } from "../../components/ui/button";
@ -10,18 +10,43 @@ import {
inputHandlerEventType,
loginInputStateType,
} from "../../types/components";
import { onLogin } from "../../controllers/API";
import { LoginType } from "../../types/api";
import { AuthContext } from "../../contexts/authContext";
export default function LoginPage(): JSX.Element {
const [inputState, setInputState] =
useState<loginInputStateType>(CONTROL_LOGIN_STATE);
const { password, username } = inputState;
const { login } = useContext(AuthContext);
const navigate = useNavigate();
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
function signIn(){
const user: LoginType = {
username: username,
password: password
};
try{
onLogin(
user
).then((user) => {
login(user.access_token, user.refresh_token);
navigate("/");
});
}
catch(error){
}
}
return (
<Form.Root
onSubmit={(event) => {
@ -107,7 +132,9 @@ export default function LoginPage(): JSX.Element {
</div>
<div className="w-full">
<Form.Submit asChild>
<Button className="mr-3 mt-6 w-full">Sign in</Button>
<Button
onClick={()=>signIn()}
className="mr-3 mt-6 w-full">Sign in</Button>
</Form.Submit>
</div>
<div className="w-full">

View file

@ -1,4 +1,5 @@
import { Route, Routes } from "react-router-dom";
import { ProtectedRoute } from "./components/authGuard";
import AdminPage from "./pages/AdminPage";
import LoginAdminPage from "./pages/AdminPage/LoginPage";
import CommunityPage from "./pages/CommunityPage";
@ -11,21 +12,56 @@ import SignUp from "./pages/signUpPage";
const Router = () => {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route
path="/"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
/>
<Route path="/community" element={<CommunityPage />} />
<Route path="/flow/:id/">
<Route path="" element={<FlowPage />} />
<Route
path=""
element={
<ProtectedRoute>
<FlowPage />
</ProtectedRoute>
}
/>
</Route>
<Route path="*" element={<HomePage />} />
<Route
path="*"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
/>
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignUp />} />
<Route path="/login/admin" element={<LoginAdminPage />} />
<Route path="/admin" element={<AdminPage />} />
<Route
path="/admin"
element={
<ProtectedRoute>
<AdminPage />
</ProtectedRoute>
}
/>
<Route path="/account">
<Route path="delete" element={<DeleteAccountPage />}></Route>
<Route
path="delete"
element={
<ProtectedRoute>
<DeleteAccountPage />
</ProtectedRoute>
}
></Route>
</Route>
</Routes>
);

View file

@ -62,3 +62,18 @@ export type UploadFileTypeAPI = {
file_path: string;
flowId: string;
};
export type LoginType = {
grant_type?: string;
username: string;
password: string;
scrope?: string;
client_id?: string;
client_secret?: string;
};
export type LoginAuthType = {
access_token: string;
refresh_token: string;
token_type?: string;
};

View file

@ -1,6 +1,7 @@
export type AuthContextType = {
isAuthenticated: boolean;
accessToken: string | null;
refreshToken: string | null;
login: (accessToken: string, refreshToken: string) => void;
logout: () => void;
refreshAccessToken: (refreshToken: string) => Promise<void>;