mirror of
https://github.com/bubblecup-12/VogelSocialMedia.git
synced 2025-07-10 13:18:48 +00:00
tokens refresh when jwt is expired and added basic axios config
This commit is contained in:
parent
c48498af95
commit
fbf645ba0f
15 changed files with 470 additions and 289 deletions
94
code/frontend/src/api/Auth.tsx
Normal file
94
code/frontend/src/api/Auth.tsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
ReactNode,
|
||||
FC,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import api from "./axios";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { redirectToLogin } from "./axios";
|
||||
import { refreshToken } from "./refreshToken";
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
type AuthContextType = {
|
||||
user: User | null;
|
||||
logout: () => Promise<void>;
|
||||
setUserState: (tryRefresh?: boolean) => Promise<void>;
|
||||
};
|
||||
type JwtPayload = {
|
||||
username: string;
|
||||
role: string;
|
||||
sub: string;
|
||||
jti: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
iss: string;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
type AuthProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const Auth: FC<AuthProviderProps> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
useEffect(() => {
|
||||
setUserState();
|
||||
}, []);
|
||||
|
||||
const setUserState = async (tryRefresh = true) => {
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
const now = Date.now() / 1000;
|
||||
const decoded = jwtDecode<JwtPayload>(token);
|
||||
|
||||
if (decoded.exp > now) {
|
||||
setUser({
|
||||
id: decoded.sub,
|
||||
username: decoded.username,
|
||||
role: decoded.role,
|
||||
});
|
||||
} else if (tryRefresh) {
|
||||
await refreshToken();
|
||||
await setUserState(false);
|
||||
}
|
||||
} catch {
|
||||
console.log("Error while reading and refreshing user info"); // this error should only appear if the app is run in strictMode
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await api.delete("/user/logout");
|
||||
} catch {
|
||||
console.log("Logout error");
|
||||
}
|
||||
localStorage.clear();
|
||||
setUser(null);
|
||||
redirectToLogin(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, logout, setUserState }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return ctx;
|
||||
};
|
67
code/frontend/src/api/axios.ts
Normal file
67
code/frontend/src/api/axios.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import axios from "axios";
|
||||
import { refreshToken } from "./refreshToken";
|
||||
|
||||
const excludedUrls: string[] = ["/user/login", "/user/regiser"];
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "http://localhost:3001/api",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// get token from local storage
|
||||
const getAccessToken = () => localStorage.getItem("token");
|
||||
const getRefreshToken = () => localStorage.getItem("refreshToken");
|
||||
|
||||
//redirects the page to the login and back
|
||||
export const redirectToLogin = (returnToPage = true) => {
|
||||
if (returnToPage) {
|
||||
const returnTo = window.location.pathname + window.location.search;
|
||||
window.location.href = `/login?returnTo=${encodeURIComponent(returnTo)}`;
|
||||
} else {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
};
|
||||
|
||||
// Request interceptor add token
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = getAccessToken();
|
||||
if (token && config.headers) {
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// retry with new token
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
const isExcluded = excludedUrls.some((url) =>
|
||||
originalRequest.url?.includes(url)
|
||||
);
|
||||
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
!originalRequest._retry &&
|
||||
!isExcluded
|
||||
) {
|
||||
await refreshToken();
|
||||
originalRequest._retry = true;
|
||||
return api(originalRequest);
|
||||
}
|
||||
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
originalRequest._retry &&
|
||||
!isExcluded
|
||||
) {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("refreshToken");
|
||||
redirectToLogin();
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
28
code/frontend/src/api/refreshToken.ts
Normal file
28
code/frontend/src/api/refreshToken.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import axios from "axios";
|
||||
const getRefreshToken = () => localStorage.getItem("refreshToken");
|
||||
|
||||
export const refreshToken = async () => {
|
||||
const token = getRefreshToken();
|
||||
|
||||
if (!token) {
|
||||
throw new Error("No refresh token available");
|
||||
}
|
||||
const response = await axios.get(
|
||||
"http://localhost:3001/api/user/refreshToken",
|
||||
{
|
||||
headers: {
|
||||
"Refresh-Token": getRefreshToken(),
|
||||
},
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
const authHeader = response.headers["authorization"];
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
const token = authHeader.substring(7);
|
||||
localStorage.setItem("token", token);
|
||||
}
|
||||
const refreshToken = response.headers["refresh-token"];
|
||||
if (refreshToken) {
|
||||
localStorage.setItem("refreshToken", refreshToken);
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue