refactor(auth): separate auth and author responsibilities and centralize auth client creation
- Replace manual axios auth client with createApiClient in auth context - Decouple domain author logic from auth provider - Make AuthorModel extend AuthUser explicitly - Route login/register/logout exclusively through auth package - Derive application-level currentUser from auth identity - Fix provider hierarchy and hook usage across Blog and Profile - Align main.jsx to use base AuthProvider + AuthorProvider layering
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
|
||||||
import { tokenStore } from "./token";
|
import { tokenStore } from "./token";
|
||||||
import { attachAuthInterceptors } from "./axios";
|
import { createApiClient } from "./axios";
|
||||||
import { AuthUser } from "./models";
|
import { AuthUser } from "./models";
|
||||||
|
|
||||||
interface AuthContextModel {
|
interface AuthContextModel {
|
||||||
@@ -19,23 +18,16 @@ const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
|||||||
export function AuthProvider({
|
export function AuthProvider({
|
||||||
children,
|
children,
|
||||||
authBaseUrl,
|
authBaseUrl,
|
||||||
apiClient,
|
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
authBaseUrl: string;
|
authBaseUrl: string;
|
||||||
apiClient: any; // your domain api client
|
|
||||||
}) {
|
}) {
|
||||||
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
|
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
|
||||||
const [token, setToken] = useState<string | null>(tokenStore.get());
|
const [token, setToken] = useState<string | null>(tokenStore.get());
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const auth = axios.create({
|
const auth = createApiClient(authBaseUrl);
|
||||||
baseURL: authBaseUrl,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
|
|
||||||
attachAuthInterceptors(auth);
|
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +53,6 @@ export function AuthProvider({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
await auth.post("/register", { username, password });
|
await auth.post("/register", { username, password });
|
||||||
await apiClient.post("/authors", { name: null, avatar: null });
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.response?.data?.detail ?? "Registration failed");
|
setError(e.response?.data?.detail ?? "Registration failed");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -79,8 +70,7 @@ export function AuthProvider({
|
|||||||
if (!token) return;
|
if (!token) return;
|
||||||
try {
|
try {
|
||||||
const me = await auth.get("/me");
|
const me = await auth.get("/me");
|
||||||
const author = await apiClient.get("/authors/me");
|
setCurrentUser({ ...me.data });
|
||||||
setCurrentUser({ ...me.data, ...author.data });
|
|
||||||
} catch {
|
} catch {
|
||||||
logout();
|
logout();
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { AuthProvider, useAuth } from "./context";
|
export { AuthProvider, useAuth } from "./contexts";
|
||||||
export { createApiClient } from "./axios";
|
export { createApiClient } from "./axios";
|
||||||
export { AuthPage } from "./AuthPage";
|
export { AuthPage } from "./AuthPage";
|
||||||
|
export type { AuthUser } from "./models";
|
||||||
export type { AuthMode } from "./AuthPage";
|
export type { AuthMode } from "./AuthPage";
|
||||||
export { tokenStore } from "./token"
|
export { tokenStore } from "./token"
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import Latest from './components/Latest';
|
|||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import Profile from './components/Profile';
|
import Profile from './components/Profile';
|
||||||
import { useArticles } from './providers/Article';
|
import { useArticles } from './providers/Article';
|
||||||
import { useAuth } from './providers/Author';
|
import { useAuth as useAuthor } from './providers/Author';
|
||||||
import { View, useViewRouter } from "./types/views";
|
import { View, useViewRouter } from "./types/views";
|
||||||
import { ArticleModel, ArticlesModel } from "./types/models";
|
import { ArticleModel, ArticlesModel } from "./types/models";
|
||||||
import { ArticleViewProps, ArticleEditorProps } from "./types/props";
|
import { ArticleViewProps, ArticleEditorProps } from "./types/props";
|
||||||
|
|
||||||
import { AuthPage, AuthMode } from '../../auth/src';
|
import { useAuth, AuthPage, AuthMode } from '../../auth/src';
|
||||||
|
|
||||||
function HomeView({
|
function HomeView({
|
||||||
currentUser,
|
currentUser,
|
||||||
@@ -55,7 +55,7 @@ function HomeView({
|
|||||||
export default function Blog(props: { disableCustomTheme?: boolean }) {
|
export default function Blog(props: { disableCustomTheme?: boolean }) {
|
||||||
const { articles, loading, error } = useArticles();
|
const { articles, loading, error } = useArticles();
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const { currentUser } = auth;
|
const { currentUser } = useAuthor();
|
||||||
|
|
||||||
const [ui, setUI] = React.useState({
|
const [ui, setUI] = React.useState({
|
||||||
selectedArticle: null as ArticleModel | null,
|
selectedArticle: null as ArticleModel | null,
|
||||||
@@ -121,7 +121,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
register: auth.register,
|
register: auth.register,
|
||||||
loading: auth.loading,
|
loading: auth.loading,
|
||||||
error: auth.error,
|
error: auth.error,
|
||||||
currentUser: auth.currentUser,
|
currentUser: currentUser,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
import { useAuth } from '../providers/Author';
|
import { useAuth as useAuthor } from '../providers/Author';
|
||||||
|
import { useAuth } from '../../../auth/src';
|
||||||
import { useUpload } from "../providers/Upload";
|
import { useUpload } from "../providers/Upload";
|
||||||
import ImageUploadField from './ImageUploadField';
|
import ImageUploadField from './ImageUploadField';
|
||||||
import { ProfileProps } from '../types/props';
|
import { ProfileProps } from '../types/props';
|
||||||
@@ -17,7 +18,9 @@ import { ProfileProps } from '../types/props';
|
|||||||
export default function Profile({
|
export default function Profile({
|
||||||
onBack
|
onBack
|
||||||
}: ProfileProps) {
|
}: ProfileProps) {
|
||||||
const { currentUser, loading, error, logout, updateProfile } = useAuth();
|
const { logout } = useAuth();
|
||||||
|
const { currentUser, updateProfile, loading, error } = useAuthor();
|
||||||
|
|
||||||
const { uploadFile } = useUpload();
|
const { uploadFile } = useUpload();
|
||||||
const [formData, setFormData] = React.useState({
|
const [formData, setFormData] = React.useState({
|
||||||
username: currentUser?.username || '',
|
username: currentUser?.username || '',
|
||||||
|
|||||||
@@ -1,71 +1,50 @@
|
|||||||
import React, { createContext, useState, useEffect, useContext } from "react";
|
import React, { createContext, useState, useEffect, useContext } from "react";
|
||||||
import { api, auth } from "../utils/api";
|
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";
|
||||||
import { tokenStore } from "../../../auth/src";
|
import { useAuth as useBaseAuth } from "../../../auth/src";
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { currentUser: authUser, logout } = useBaseAuth();
|
||||||
const [currentUser, setCurrentUser] = useState<AuthorModel | null>(null);
|
const [currentUser, setCurrentUser] = useState<AuthorModel | null>(null);
|
||||||
const [authors, setAuthors] = useState<AuthorModel[]>([]);
|
const [authors, setAuthors] = useState<AuthorModel[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
/** 🔹 Register new user */
|
/**
|
||||||
const register = async (username: string, password: string) => {
|
* Hydrate application-level currentUser
|
||||||
|
*/
|
||||||
|
const hydrateCurrentUser = async () => {
|
||||||
|
if (!authUser) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const res = await auth.post('/register', { username, password });
|
const res = await api.get<AuthorModel>("/authors/me");
|
||||||
|
|
||||||
// auto-login
|
/**
|
||||||
await login(username, password);
|
* Explicit precedence:
|
||||||
|
* Auth service is source of truth for inherited fields
|
||||||
|
*/
|
||||||
|
const fullUser: AuthorModel = {
|
||||||
|
...res.data,
|
||||||
|
username: authUser.username,
|
||||||
|
email: authUser.email,
|
||||||
|
is_active: authUser.is_active,
|
||||||
|
};
|
||||||
|
|
||||||
// now create author
|
setCurrentUser(fullUser);
|
||||||
await api.post('/authors', { name: null, avatar: null });
|
} catch (err) {
|
||||||
|
console.error("Failed to hydrate current user:", err);
|
||||||
return res.data;
|
logout();
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Registration failed:', err);
|
|
||||||
setError(err.response?.data?.detail || 'Registration failed');
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 🔹 Login and store JWT token */
|
|
||||||
const login = async (username: string, password: string) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const res = await auth.post('/login', { username, password });
|
|
||||||
const { access_token } = res.data;
|
|
||||||
|
|
||||||
if (!access_token) {
|
|
||||||
throw new Error("No access token returned");
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenStore.set(access_token);
|
|
||||||
|
|
||||||
await fetchCurrentUser();
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Login failed:', err);
|
|
||||||
setError(err.response?.data?.detail || 'Invalid credentials');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 🔹 Logout and clear everything */
|
|
||||||
const logout = () => {
|
|
||||||
tokenStore.clear();
|
|
||||||
setCurrentUser(null);
|
|
||||||
setAuthors([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 🔹 Fetch all authors (JWT handled by api interceptor) */
|
/** 🔹 Fetch all authors (JWT handled by api interceptor) */
|
||||||
const refreshAuthors = async () => {
|
const refreshAuthors = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -103,28 +82,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 🔹 Auto-load current user if token exists */
|
/**
|
||||||
const fetchCurrentUser = async () => {
|
* React strictly to auth lifecycle
|
||||||
try {
|
*/
|
||||||
const me = await auth.get('/me');
|
|
||||||
|
|
||||||
const author = await api.get<AuthorModel>(`/authors/me`);
|
|
||||||
|
|
||||||
const fullUser = { ...me.data, ...author.data };
|
|
||||||
|
|
||||||
setCurrentUser(fullUser);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch current user:', err);
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 🔹 On mount, try to fetch user if token exists */
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tokenStore.get()) {
|
if (authUser) {
|
||||||
fetchCurrentUser();
|
hydrateCurrentUser();
|
||||||
|
} else {
|
||||||
|
setCurrentUser(null);
|
||||||
|
setAuthors([]);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [authUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
@@ -133,9 +101,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
authors,
|
authors,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
register,
|
|
||||||
refreshAuthors,
|
refreshAuthors,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export interface AuthContextModel {
|
|||||||
authors: AuthorModel[];
|
authors: AuthorModel[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
login: (username: string, password: string) => Promise<void>;
|
|
||||||
register: (username: string, password: string) => Promise<void>;
|
|
||||||
logout: () => void;
|
|
||||||
refreshAuthors: () => Promise<void>;
|
refreshAuthors: () => Promise<void>;
|
||||||
updateProfile: (user: AuthorModel) => Promise<AuthorModel | void>;
|
updateProfile: (user: AuthorModel) => Promise<AuthorModel | void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,18 @@ import {
|
|||||||
createInList, readInList, updateInList, deleteInList,
|
createInList, readInList, updateInList, deleteInList,
|
||||||
createById, readById, updateById, deleteById
|
createById, readById, updateById, deleteById
|
||||||
} from "../utils/articles";
|
} from "../utils/articles";
|
||||||
|
import { AuthUser } from "../../../auth/src";
|
||||||
|
|
||||||
|
|
||||||
export interface AuthorModel {
|
export interface AuthorModel extends AuthUser {
|
||||||
// meta fields
|
// meta fields
|
||||||
_id?: string | null;
|
_id?: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|
||||||
// model fields
|
// model fields
|
||||||
username: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
|
||||||
avatar: string;
|
avatar: string;
|
||||||
is_active: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArticleModel {
|
export interface ArticleModel {
|
||||||
|
|||||||
10
src/main.jsx
10
src/main.jsx
@@ -2,18 +2,22 @@ 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';
|
import { AuthProvider as AuthorProvider } from './blog/providers/Author';
|
||||||
import { UploadProvider } from "./blog/providers/Upload";
|
import { UploadProvider } from "./blog/providers/Upload";
|
||||||
|
import { AuthProvider } from "../auth/src";
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
|
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL;
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<UploadProvider>
|
<UploadProvider>
|
||||||
<AuthProvider>
|
<AuthProvider authBaseUrl={AUTH_BASE}>
|
||||||
|
<AuthorProvider>
|
||||||
<ArticleProvider>
|
<ArticleProvider>
|
||||||
<Blog />
|
<Blog />
|
||||||
</ArticleProvider>
|
</ArticleProvider>
|
||||||
|
</AuthorProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</UploadProvider>,
|
</UploadProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user