🔧 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:
parent
c30cb3e002
commit
c0f586ad89
11 changed files with 240 additions and 42 deletions
14
src/frontend/src/components/authGuard/index.tsx
Normal file
14
src/frontend/src/components/authGuard/index.tsx
Normal 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;
|
||||
};
|
||||
|
|
@ -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 !== "" && (
|
||||
|
|
|
|||
|
|
@ -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: "",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue