jwt provider and common api utils
This commit is contained in:
@@ -1,28 +1,27 @@
|
|||||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||||
import axios from 'axios';
|
import { api } from '../utils/api';
|
||||||
import { ArticleModel } from "../types/models";
|
import { ArticleModel } from '../types/models';
|
||||||
import { ArticleContextModel } from "../types/contexts";
|
import { ArticleContextModel } from '../types/contexts';
|
||||||
|
import { useAuth } from './Author';
|
||||||
|
|
||||||
const ArticleContext = createContext<ArticleContextModel | undefined>(undefined);
|
const ArticleContext = createContext<ArticleContextModel | undefined>(undefined);
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
|
||||||
|
|
||||||
export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [articles, setArticles] = useState<ArticleModel[]>([]);
|
const [articles, setArticles] = useState<ArticleModel[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { token } = useAuth(); // ✅ access token if needed
|
||||||
|
|
||||||
|
/** 🔹 Fetch articles (JWT automatically attached by api.ts interceptor) */
|
||||||
const fetchArticles = async () => {
|
const fetchArticles = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// ✅ Use correct full endpoint from OpenAPI spec
|
const res = await api.get<ArticleModel[]>('/articles', {
|
||||||
const res = await axios.get<ArticleModel[]>(`${API_BASE}/articles`, {
|
|
||||||
params: { skip: 0, limit: 10 },
|
params: { skip: 0, limit: 10 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ Normalize if backend sends _id instead of id
|
|
||||||
const formatted = res.data.map((a) => ({
|
const formatted = res.data.map((a) => ({
|
||||||
...a,
|
...a,
|
||||||
id: a._id || undefined,
|
id: a._id || undefined,
|
||||||
@@ -31,15 +30,16 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
|||||||
setArticles(formatted);
|
setArticles(formatted);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch articles:', err);
|
console.error('Failed to fetch articles:', err);
|
||||||
setError(err.message || 'Failed to fetch articles');
|
setError(err.response?.data?.detail || 'Failed to fetch articles');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 🔹 Auto-fetch articles whenever user logs in/out */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchArticles();
|
if (token) fetchArticles();
|
||||||
}, []);
|
}, [token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArticleContext.Provider value={{ articles, loading, error, refreshArticles: fetchArticles }}>
|
<ArticleContext.Provider value={{ articles, loading, error, refreshArticles: fetchArticles }}>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import React, { createContext, useState, useEffect, useContext } from 'react';
|
import React, { createContext, useState, useEffect, useContext } from 'react';
|
||||||
import axios from 'axios';
|
import { api } from '../utils/api';
|
||||||
import { AuthorModel } from "../types/models";
|
import { AuthorModel } from '../types/models';
|
||||||
import { AuthContextModel } from "../types/contexts";
|
import { AuthContextModel } from '../types/contexts';
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [currentUser, setCurrentUser] = useState<AuthorModel | null>(null);
|
const [currentUser, setCurrentUser] = useState<AuthorModel | null>(null);
|
||||||
const [authors, setAuthors] = useState<AuthorModel[]>([]);
|
const [authors, setAuthors] = useState<AuthorModel[]>([]);
|
||||||
@@ -20,7 +18,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const res = await axios.post(`${API_BASE}/auth/login`, { email, password });
|
const res = await api.post('/auth/login', { email, password });
|
||||||
const { access_token, user } = res.data;
|
const { access_token, user } = res.data;
|
||||||
|
|
||||||
if (access_token) {
|
if (access_token) {
|
||||||
@@ -44,17 +42,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
setAuthors([]);
|
setAuthors([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 🔹 Fetch all authors (requires valid JWT) */
|
/** 🔹 Fetch all authors (JWT handled by api interceptor) */
|
||||||
const refreshAuthors = async () => {
|
const refreshAuthors = async () => {
|
||||||
if (!token) return;
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const res = await axios.get<AuthorModel[]>(`${API_BASE}/authors`, {
|
const res = await api.get<AuthorModel[]>('/authors');
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
setAuthors(res.data);
|
setAuthors(res.data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch authors:', err);
|
console.error('Failed to fetch authors:', err);
|
||||||
@@ -68,9 +62,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const fetchCurrentUser = async () => {
|
const fetchCurrentUser = async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<AuthorModel>(`${API_BASE}/auth/me`, {
|
const res = await api.get<AuthorModel>('/auth/me');
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
setCurrentUser(res.data);
|
setCurrentUser(res.data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch current user:', err);
|
console.error('Failed to fetch current user:', err);
|
||||||
@@ -78,6 +70,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 🔹 On mount, try to fetch user if token exists */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) fetchCurrentUser();
|
if (token) fetchCurrentUser();
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|||||||
33
src/blog/utils/api.ts
Normal file
33
src/blog/utils/api.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// src/utils/api.ts
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: API_BASE,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔹 Attach token from localStorage before each request
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔹 Handle expired or invalid tokens globally
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
console.warn('Token expired or invalid. Logging out...');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
// Optionally: trigger a redirect or event
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -2,14 +2,17 @@ import * as React from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import Blog from './blog/Blog';
|
import Blog from './blog/Blog';
|
||||||
import { ArticleProvider } from './blog/providers/Article';
|
import { ArticleProvider } from './blog/providers/Article';
|
||||||
|
import { AuthProvider } from './blog/providers/Author';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ArticleProvider>
|
<AuthProvider>
|
||||||
<Blog />
|
<ArticleProvider>
|
||||||
</ArticleProvider>
|
<Blog />
|
||||||
|
</ArticleProvider>
|
||||||
|
</AuthProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user