diff --git a/auth/src/context.tsx b/auth/src/contexts.tsx similarity index 83% rename from auth/src/context.tsx rename to auth/src/contexts.tsx index a915b89..f12ba4a 100644 --- a/auth/src/context.tsx +++ b/auth/src/contexts.tsx @@ -1,7 +1,6 @@ import React, { createContext, useContext, useEffect, useState } from "react"; -import axios from "axios"; import { tokenStore } from "./token"; -import { attachAuthInterceptors } from "./axios"; +import { createApiClient } from "./axios"; import { AuthUser } from "./models"; interface AuthContextModel { @@ -19,23 +18,16 @@ const AuthContext = createContext(undefined); export function AuthProvider({ children, authBaseUrl, - apiClient, }: { children: React.ReactNode; authBaseUrl: string; - apiClient: any; // your domain api client }) { const [currentUser, setCurrentUser] = useState(null); const [token, setToken] = useState(tokenStore.get()); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const auth = axios.create({ - baseURL: authBaseUrl, - headers: { "Content-Type": "application/json" }, - }); - - attachAuthInterceptors(auth); + const auth = createApiClient(authBaseUrl); const login = async (username: string, password: string) => { try { @@ -61,7 +53,6 @@ export function AuthProvider({ setError(null); await auth.post("/register", { username, password }); - await apiClient.post("/authors", { name: null, avatar: null }); } catch (e: any) { setError(e.response?.data?.detail ?? "Registration failed"); } finally { @@ -79,8 +70,7 @@ export function AuthProvider({ if (!token) return; try { const me = await auth.get("/me"); - const author = await apiClient.get("/authors/me"); - setCurrentUser({ ...me.data, ...author.data }); + setCurrentUser({ ...me.data }); } catch { logout(); } diff --git a/auth/src/index.ts b/auth/src/index.ts index 9dc4ed6..3fe14d6 100644 --- a/auth/src/index.ts +++ b/auth/src/index.ts @@ -1,5 +1,6 @@ -export { AuthProvider, useAuth } from "./context"; +export { AuthProvider, useAuth } from "./contexts"; export { createApiClient } from "./axios"; export { AuthPage } from "./AuthPage"; +export type { AuthUser } from "./models"; export type { AuthMode } from "./AuthPage"; export { tokenStore } from "./token" diff --git a/src/blog/Blog.tsx b/src/blog/Blog.tsx index 308e604..f0146d4 100644 --- a/src/blog/Blog.tsx +++ b/src/blog/Blog.tsx @@ -12,12 +12,12 @@ import Latest from './components/Latest'; import Footer from './components/Footer'; import Profile from './components/Profile'; import { useArticles } from './providers/Article'; -import { useAuth } from './providers/Author'; +import { useAuth as useAuthor } from './providers/Author'; import { View, useViewRouter } from "./types/views"; import { ArticleModel, ArticlesModel } from "./types/models"; import { ArticleViewProps, ArticleEditorProps } from "./types/props"; -import { AuthPage, AuthMode } from '../../auth/src'; +import { useAuth, AuthPage, AuthMode } from '../../auth/src'; function HomeView({ currentUser, @@ -55,7 +55,7 @@ function HomeView({ export default function Blog(props: { disableCustomTheme?: boolean }) { const { articles, loading, error } = useArticles(); const auth = useAuth(); - const { currentUser } = auth; + const { currentUser } = useAuthor(); const [ui, setUI] = React.useState({ selectedArticle: null as ArticleModel | null, @@ -121,7 +121,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { register: auth.register, loading: auth.loading, error: auth.error, - currentUser: auth.currentUser, + currentUser: currentUser, }), }, diff --git a/src/blog/components/Profile.tsx b/src/blog/components/Profile.tsx index a5710c1..ffa9c4e 100644 --- a/src/blog/components/Profile.tsx +++ b/src/blog/components/Profile.tsx @@ -9,7 +9,8 @@ import { Alert, } from '@mui/material'; 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 ImageUploadField from './ImageUploadField'; import { ProfileProps } from '../types/props'; @@ -17,7 +18,9 @@ import { ProfileProps } from '../types/props'; export default function Profile({ onBack }: ProfileProps) { - const { currentUser, loading, error, logout, updateProfile } = useAuth(); + const { logout } = useAuth(); + const { currentUser, updateProfile, loading, error } = useAuthor(); + const { uploadFile } = useUpload(); const [formData, setFormData] = React.useState({ username: currentUser?.username || '', diff --git a/src/blog/providers/Author.tsx b/src/blog/providers/Author.tsx index b4f67f5..5392843 100644 --- a/src/blog/providers/Author.tsx +++ b/src/blog/providers/Author.tsx @@ -1,71 +1,50 @@ 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 { AuthContextModel } from "../types/contexts"; -import { tokenStore } from "../../../auth/src"; +import { useAuth as useBaseAuth } from "../../../auth/src"; const AuthContext = createContext(undefined); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { currentUser: authUser, logout } = useBaseAuth(); const [currentUser, setCurrentUser] = useState(null); const [authors, setAuthors] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - /** 🔹 Register new user */ - const register = async (username: string, password: string) => { + /** + * Hydrate application-level currentUser + */ + const hydrateCurrentUser = async () => { + if (!authUser) return; + try { setLoading(true); setError(null); - const res = await auth.post('/register', { username, password }); + const res = await api.get("/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 - await api.post('/authors', { name: null, avatar: null }); - - return res.data; - } catch (err: any) { - console.error('Registration failed:', err); - setError(err.response?.data?.detail || 'Registration failed'); + setCurrentUser(fullUser); + } catch (err) { + console.error("Failed to hydrate current user:", err); + logout(); } finally { 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) */ const refreshAuthors = async () => { try { @@ -103,28 +82,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } }; - /** 🔹 Auto-load current user if token exists */ - const fetchCurrentUser = async () => { - try { - const me = await auth.get('/me'); - - const author = await api.get(`/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 */ + /** + * React strictly to auth lifecycle + */ useEffect(() => { - if (tokenStore.get()) { - fetchCurrentUser(); + if (authUser) { + hydrateCurrentUser(); + } else { + setCurrentUser(null); + setAuthors([]); } - }, []); + }, [authUser]); return ( = ({ children authors, loading, error, - login, - logout, - register, + refreshAuthors, updateProfile, }} diff --git a/src/blog/types/contexts.ts b/src/blog/types/contexts.ts index 1c3a362..12094f7 100644 --- a/src/blog/types/contexts.ts +++ b/src/blog/types/contexts.ts @@ -18,9 +18,7 @@ export interface AuthContextModel { authors: AuthorModel[]; loading: boolean; error: string | null; - login: (username: string, password: string) => Promise; - register: (username: string, password: string) => Promise; - logout: () => void; + refreshAuthors: () => Promise; updateProfile: (user: AuthorModel) => Promise; } diff --git a/src/blog/types/models.ts b/src/blog/types/models.ts index f0413a2..4de53bf 100644 --- a/src/blog/types/models.ts +++ b/src/blog/types/models.ts @@ -2,20 +2,18 @@ import { createInList, readInList, updateInList, deleteInList, createById, readById, updateById, deleteById } from "../utils/articles"; +import { AuthUser } from "../../../auth/src"; -export interface AuthorModel { +export interface AuthorModel extends AuthUser { // meta fields _id?: string | null; created_at: string; updated_at: string; // model fields - username: string; name: string; - email: string; avatar: string; - is_active: boolean; } export interface ArticleModel { diff --git a/src/main.jsx b/src/main.jsx index 1becdc0..33e40a8 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,18 +2,22 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import Blog from './blog/Blog'; 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 { AuthProvider } from "../auth/src"; const rootElement = document.getElementById('root'); const root = createRoot(rootElement); +const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL; root.render( - - - - + + + + + + - , + );