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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow"; import { ReactFlowJsonObject } from "reactflow";
import { api } from "../../controllers/API/api"; 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 { FlowStyleType, FlowType } from "../../types/flow";
import { import {
APIClassType, APIClassType,
@ -346,3 +346,43 @@ export async function postCustomComponent(
): Promise<AxiosResponse<APIClassType>> { ): Promise<AxiosResponse<APIClassType>> {
return await api.post(`/api/v1/custom_component`, { code }); 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 * as Form from "@radix-ui/react-form";
import { useState } from "react"; import { useContext, useState } from "react";
import { Link } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent"; import InputComponent from "../../components/inputComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
@ -10,18 +10,43 @@ import {
inputHandlerEventType, inputHandlerEventType,
loginInputStateType, loginInputStateType,
} from "../../types/components"; } from "../../types/components";
import { onLogin } from "../../controllers/API";
import { LoginType } from "../../types/api";
import { AuthContext } from "../../contexts/authContext";
export default function LoginPage(): JSX.Element { export default function LoginPage(): JSX.Element {
const [inputState, setInputState] = const [inputState, setInputState] =
useState<loginInputStateType>(CONTROL_LOGIN_STATE); useState<loginInputStateType>(CONTROL_LOGIN_STATE);
const { password, username } = inputState; const { password, username } = inputState;
const { login } = useContext(AuthContext);
const navigate = useNavigate();
function handleInput({ function handleInput({
target: { name, value }, target: { name, value },
}: inputHandlerEventType): void { }: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value })); 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 ( return (
<Form.Root <Form.Root
onSubmit={(event) => { onSubmit={(event) => {
@ -107,7 +132,9 @@ export default function LoginPage(): JSX.Element {
</div> </div>
<div className="w-full"> <div className="w-full">
<Form.Submit asChild> <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> </Form.Submit>
</div> </div>
<div className="w-full"> <div className="w-full">

View file

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

View file

@ -62,3 +62,18 @@ export type UploadFileTypeAPI = {
file_path: string; file_path: string;
flowId: 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 = { export type AuthContextType = {
isAuthenticated: boolean; isAuthenticated: boolean;
accessToken: string | null; accessToken: string | null;
refreshToken: string | null;
login: (accessToken: string, refreshToken: string) => void; login: (accessToken: string, refreshToken: string) => void;
logout: () => void; logout: () => void;
refreshAccessToken: (refreshToken: string) => Promise<void>; refreshAccessToken: (refreshToken: string) => Promise<void>;