From 2c18c7258b7ab069f8236dd6c48d573d1cbb688b Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 26 Dec 2025 00:56:23 +0530 Subject: [PATCH 1/5] refactor(auth): extract auth into shared package and unify auth flow - Introduce new @local/auth package with token store, axios helpers, auth client, and AuthPage UI - Unify login/register into a single AuthPage with mode switching - Centralize JWT handling via tokenStore and axios interceptors - Remove direct localStorage token access from blog app - Replace blog Login/Register views with single auth view - Update router (View, VIEW_TREE, VIEW_URL) to support unified auth view - Fix hook usage by lifting useAuth() to top-level and passing via props - Refactor Blog view navigation to support auth mode routing - Clean up ArticleProvider to rely on auth state, not tokens - Align AuthProvider to delegate token management to auth package - Remove legacy Login/Register components and props - Normalize API client creation via shared createApiClient - Improve type safety and state consistency across auth/article flows --- auth/package.json | 11 ++ .../Login.tsx => auth/src/AuthPage.tsx | 60 +++++++--- auth/src/authClient.ts | 32 +++++ auth/src/axios.ts | 35 ++++++ auth/src/context.tsx | 111 +++++++++++++++++ auth/src/index.ts | 5 + auth/src/props.ts | 0 auth/src/token.ts | 15 +++ src/blog/Blog.tsx | 70 +++++++---- src/blog/components/Register.tsx | 113 ------------------ src/blog/providers/Article.tsx | 22 ++-- src/blog/providers/Author.tsx | 37 +++--- src/blog/types/contexts.ts | 1 - src/blog/types/props.ts | 9 -- src/blog/types/views.ts | 26 ++-- src/blog/utils/api.ts | 62 +++------- 16 files changed, 355 insertions(+), 254 deletions(-) create mode 100644 auth/package.json rename src/blog/components/Login.tsx => auth/src/AuthPage.tsx (65%) create mode 100644 auth/src/authClient.ts create mode 100644 auth/src/axios.ts create mode 100644 auth/src/context.tsx create mode 100644 auth/src/index.ts create mode 100644 auth/src/props.ts create mode 100644 auth/src/token.ts delete mode 100644 src/blog/components/Register.tsx diff --git a/auth/package.json b/auth/package.json new file mode 100644 index 0000000..4a14219 --- /dev/null +++ b/auth/package.json @@ -0,0 +1,11 @@ +{ + "name": "@local/auth", + "version": "0.1.0", + "private": true, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "peerDependencies": { + "react": "^18", + "react-dom": "^18" + } +} diff --git a/src/blog/components/Login.tsx b/auth/src/AuthPage.tsx similarity index 65% rename from src/blog/components/Login.tsx rename to auth/src/AuthPage.tsx index 6bba4cf..c426240 100644 --- a/src/blog/components/Login.tsx +++ b/auth/src/AuthPage.tsx @@ -1,26 +1,49 @@ import * as React from 'react'; import { Box, TextField, Button, Typography, IconButton, CircularProgress, Link } from '@mui/material'; import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; -import { useAuth } from '../providers/Author'; -import { LoginProps } from '../types/props'; -export default function Login({ +export type AuthMode = "login" | "register"; + +export interface AuthPageProps { + mode: AuthMode; + onBack(): void; + onSwitchMode(): void; + login(username: string, password: string): Promise; + register(username: string, password: string): Promise; + loading: boolean; + error: string | null; + currentUser: any; +} + +export function AuthPage({ + mode, onBack, - onRegister -}: LoginProps) { - const { login, loading, error, currentUser } = useAuth(); + onSwitchMode, + login, + register, + loading, + error, + currentUser, +}: AuthPageProps) { + const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); + const isLogin = mode === "login"; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await login(username, password); + if (isLogin) { + await login(username, password); + } else { + await register(username, password); + } }; // ✅ Auto-return if already logged in React.useEffect(() => { if (currentUser) onBack(); - }, [currentUser]); + }, [currentUser, onBack]); return ( - Sign In + {isLogin ? "Sign In" : "Create Account"} - Please log in to continue + {isLogin + ? "Please log in to continue" + : "Create an account to get started"}
@@ -55,6 +80,7 @@ export default function Login({ value={username} onChange={(e) => setUsername(e.target.value)} required + autoFocus /> - {loading ? : 'Login'} + {loading ? ( + + ) : isLogin ? ( + "Login" + ) : ( + "Register" + )} @@ -90,15 +122,15 @@ export default function Login({ align="center" sx={{ mt: 3 }} > - Don’t have an account?{' '} + {isLogin ? "Don’t have an account?" : "Already have an account?"}{' '} - Register + {isLogin ? "Register" : "Login"}
diff --git a/auth/src/authClient.ts b/auth/src/authClient.ts new file mode 100644 index 0000000..bfcc0e4 --- /dev/null +++ b/auth/src/authClient.ts @@ -0,0 +1,32 @@ +import { createApiClient } from "./axios"; +import { tokenStore } from "./token"; + +// @ts-ignore +const authApi = createApiClient(import.meta.env.VITE_AUTH_BASE_URL); + +export const authClient = { + async login(username: string, password: string) { + const res = await authApi.post("/login", { username, password }); + const { access_token } = res.data; + + if (!access_token) { + throw new Error("No access token returned"); + } + + tokenStore.set(access_token); + return this.getIdentity(); + }, + + logout() { + tokenStore.clear(); + }, + + async getIdentity() { + const res = await authApi.get("/me"); + return res.data; + }, + + isAuthenticated() { + return !!tokenStore.get(); + }, +}; diff --git a/auth/src/axios.ts b/auth/src/axios.ts new file mode 100644 index 0000000..f47154b --- /dev/null +++ b/auth/src/axios.ts @@ -0,0 +1,35 @@ +import axios, { AxiosInstance } from "axios"; +import { tokenStore } from "./token"; + +export function attachAuthInterceptors(client: AxiosInstance) { + client.interceptors.request.use((config) => { + const token = tokenStore.get(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }); + + client.interceptors.response.use( + (res) => res, + (error) => { + if (error.response?.status === 401) { + tokenStore.clear(); + } + return Promise.reject(error); + } + ); +} + +/** + * Factory for app APIs that need auth + */ +export function createApiClient(baseURL: string): AxiosInstance { + const client = axios.create({ + baseURL, + headers: { "Content-Type": "application/json" }, + }); + + attachAuthInterceptors(client); + return client; +} diff --git a/auth/src/context.tsx b/auth/src/context.tsx new file mode 100644 index 0000000..8000314 --- /dev/null +++ b/auth/src/context.tsx @@ -0,0 +1,111 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; +import axios from "axios"; +import { tokenStore } from "./token"; +import { attachAuthInterceptors } from "./axios"; + +export interface AuthUser { + _id: string; + username: string; + [key: string]: any; +} + +interface AuthContextModel { + currentUser: AuthUser | null; + token: string | null; + loading: boolean; + error: string | null; + login(username: string, password: string): Promise; + register(username: string, password: string): Promise; + logout(): void; +} + +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 login = async (username: string, password: string) => { + try { + setLoading(true); + setError(null); + + const res = await auth.post("/login", { username, password }); + const { access_token, user } = res.data; + + tokenStore.set(access_token); + setToken(access_token); + setCurrentUser(user); + } catch (e: any) { + setError(e.response?.data?.detail ?? "Login failed"); + } finally { + setLoading(false); + } + }; + + const register = async (username: string, password: string) => { + try { + setLoading(true); + 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 { + setLoading(false); + } + }; + + const logout = () => { + tokenStore.clear(); + setToken(null); + setCurrentUser(null); + }; + + const fetchCurrentUser = async () => { + if (!token) return; + try { + const me = await auth.get("/me"); + const author = await apiClient.get("/authors/me"); + setCurrentUser({ ...me.data, ...author.data }); + } catch { + logout(); + } + }; + + useEffect(() => { + fetchCurrentUser(); + }, [token]); + + return ( + + {children} + + ); +} + +export function useAuth(): AuthContextModel { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); + return ctx; +} diff --git a/auth/src/index.ts b/auth/src/index.ts new file mode 100644 index 0000000..9dc4ed6 --- /dev/null +++ b/auth/src/index.ts @@ -0,0 +1,5 @@ +export { AuthProvider, useAuth } from "./context"; +export { createApiClient } from "./axios"; +export { AuthPage } from "./AuthPage"; +export type { AuthMode } from "./AuthPage"; +export { tokenStore } from "./token" diff --git a/auth/src/props.ts b/auth/src/props.ts new file mode 100644 index 0000000..e69de29 diff --git a/auth/src/token.ts b/auth/src/token.ts new file mode 100644 index 0000000..3f5f607 --- /dev/null +++ b/auth/src/token.ts @@ -0,0 +1,15 @@ +const TOKEN_KEY = "token"; + +export const tokenStore = { + get(): string | null { + return localStorage.getItem(TOKEN_KEY); + }, + + set(token: string) { + localStorage.setItem(TOKEN_KEY, token); + }, + + clear() { + localStorage.removeItem(TOKEN_KEY); + }, +}; diff --git a/src/blog/Blog.tsx b/src/blog/Blog.tsx index 7b14258..308e604 100644 --- a/src/blog/Blog.tsx +++ b/src/blog/Blog.tsx @@ -10,8 +10,6 @@ import ArticleView from './components/Article/ArticleView'; import ArticleEditor from './components/Article/ArticleEditor'; import Latest from './components/Latest'; import Footer from './components/Footer'; -import Login from './components/Login'; -import Register from './components/Register'; import Profile from './components/Profile'; import { useArticles } from './providers/Article'; import { useAuth } from './providers/Author'; @@ -19,25 +17,29 @@ import { View, useViewRouter } from "./types/views"; import { ArticleModel, ArticlesModel } from "./types/models"; import { ArticleViewProps, ArticleEditorProps } from "./types/props"; +import { AuthPage, AuthMode } from '../../auth/src'; + function HomeView({ currentUser, - open_login, + open_auth, open_profile, open_create, articles, - openArticle + openArticle, }: any) { return ( <> - + {!currentUser ? ( - + ) : ( <> - - @@ -52,11 +54,13 @@ function HomeView({ export default function Blog(props: { disableCustomTheme?: boolean }) { const { articles, loading, error } = useArticles(); - const { currentUser } = useAuth(); + const auth = useAuth(); + const { currentUser } = auth; const [ui, setUI] = React.useState({ selectedArticle: null as ArticleModel | null, - view: "home" as View, + view: 'home' as View, + authMode: 'login' as AuthMode, }); useEffect(() => { @@ -73,6 +77,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { setUI({ selectedArticle: article, view: 'article', + authMode: 'login' }); } } @@ -85,7 +90,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { } = useViewRouter(setUI); type RouterContext = { - ui: any; + ui: typeof ui; articles: ArticlesModel; currentUser: any; openArticle: (article: ArticleModel) => void; @@ -97,20 +102,27 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { navigationMap?: Record; }; + // @ts-ignore const VIEW_COMPONENTS: Record> = { home: { component: HomeView, }, - login: { - component: Login, - navigationMap: { - open_register: 'onRegister', - }, - }, - - register: { - component: Register, + auth: { + component: AuthPage, + extraProps: ({ ui }) => ({ + mode: ui.authMode, + onSwitchMode: () => + setUI((prev) => ({ + ...prev, + authMode: prev.authMode === 'login' ? 'register' : 'login', + })), + login: auth.login, + register: auth.register, + loading: auth.loading, + error: auth.error, + currentUser: auth.currentUser, + }), }, profile: { @@ -123,6 +135,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { open_editor: 'onEdit', }, extraProps: ({ ui, articles }) => ({ + // @ts-ignore article: articles.readById(ui.selectedArticle._id), }) satisfies Partial, }, @@ -130,7 +143,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { editor: { component: ArticleEditor, extraProps: ({ ui, articles }) => ({ - article: ui.selectedArticle !== null ? articles.readById(ui.selectedArticle._id) : null, + article: ui.selectedArticle !== null ? articles.readById(ui.selectedArticle._id as string) : null, }) satisfies Partial, }, @@ -147,10 +160,15 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { const navigationMap= entry['navigationMap'] || {} const ViewComponent = entry.component; - const childNav = navigateToChildren( - ui.view, - navigationMap - ); + const childNav = { + ...navigateToChildren(ui.view, navigationMap), + open_auth: (mode: AuthMode = 'login') => + setUI((prev) => ({ + ...prev, + view: 'auth', + authMode: mode, + })), + }; const ctx: RouterContext = { ui, @@ -234,7 +252,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) { {ui.view === 'home' && ( (null); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLocalError(null); - - // ✅ Local validation - if (password1 !== password2) { - setLocalError("Passwords don't match"); - return; - } - - if (password1.length < 6) { - setLocalError('Password must be at least 6 characters long'); - return; - } - - // ✅ Call backend - await register(username, password1); - }; - - if (currentUser) { - // ✅ if logged in, auto-return to the article list - onBack(); - return null; - } - - return ( - - - - - - - Sign Up - - - - Please sign up to continue - - -
- setUsername(e.target.value)} - required - /> - setPassword1(e.target.value)} - required - /> - setPassword2(e.target.value)} - required - /> - - {(localError || error) && ( - - {localError || error} - - )} - - - -
- ); -} diff --git a/src/blog/providers/Article.tsx b/src/blog/providers/Article.tsx index 6e28585..222e3e5 100644 --- a/src/blog/providers/Article.tsx +++ b/src/blog/providers/Article.tsx @@ -1,12 +1,12 @@ -import React, { createContext, useState, useContext, useEffect } from 'react'; -import { api } from '../utils/api'; +import React, { createContext, useState, useContext, useEffect } from "react"; +import { api } from "../utils/api"; import { ArticleModel, ArticlesModel, createArticlesModelObject, -} from '../types/models'; -import { ArticleContextModel } from '../types/contexts'; -import { useAuth } from './Author'; +} from "../types/models"; +import { ArticleContextModel } from "../types/contexts"; +import { useAuth } from "./Author"; const ArticleContext = createContext(undefined); @@ -14,7 +14,7 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child const [articles, setArticles] = useState(createArticlesModelObject()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const { token, currentUser } = useAuth(); + const { currentUser } = useAuth(); /** 🔹 Author IDs must be strings for API, so we normalize here */ const normalizeArticleForApi = (article: Partial) => { @@ -109,14 +109,8 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child /** 🔹 Auto-fetch articles whenever user logs in/out */ useEffect(() => { - // Always load once on mount - // If endpoint requires JWT, fallback safely - if (!token) { - fetchArticles().catch(() => setLoading(false)); // try anyway (handles both public/protected) - } else { - fetchArticles(); - } - }, [token]); + fetchArticles(); + }, [currentUser]); // refetch on login / logout return ( (undefined); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [currentUser, setCurrentUser] = useState(null); const [authors, setAuthors] = useState([]); - const [token, setToken] = useState(localStorage.getItem('token')); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); /** 🔹 Register new user */ @@ -21,7 +21,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const res = await auth.post('/register', { username, password }); // auto-login - // await login(username, password); + await login(username, password); // now create author await api.post('/authors', { name: null, avatar: null }); @@ -42,13 +42,15 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children setError(null); const res = await auth.post('/login', { username, password }); - const { access_token, user } = res.data; + const { access_token } = res.data; - if (access_token) { - localStorage.setItem('token', access_token); - setToken(access_token); - setCurrentUser(user); + 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'); @@ -59,8 +61,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children /** 🔹 Logout and clear everything */ const logout = () => { - localStorage.removeItem('token'); - setToken(null); + tokenStore.clear(); setCurrentUser(null); setAuthors([]); }; @@ -104,7 +105,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children /** 🔹 Auto-load current user if token exists */ const fetchCurrentUser = async () => { - if (!token) return; try { const me = await auth.get('/me'); @@ -121,15 +121,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children /** 🔹 On mount, try to fetch user if token exists */ useEffect(() => { - if (token) fetchCurrentUser(); - }, [token]); + if (tokenStore.get()) { + fetchCurrentUser(); + } + }, []); return ( Promise; diff --git a/src/blog/types/props.ts b/src/blog/types/props.ts index 4432ea4..0772a44 100644 --- a/src/blog/types/props.ts +++ b/src/blog/types/props.ts @@ -9,11 +9,6 @@ export interface LatestProps { onLoadMore?: (offset: number, limit: number) => Promise; // optional async callback } -export interface LoginProps { - onBack: () => void; - onRegister: () => void; -} - export interface MainContentProps { articles: ArticlesModel; onSelectArticle: (index: ArticleModel) => void; @@ -23,10 +18,6 @@ export interface ProfileProps { onBack: () => void; } -export interface RegisterProps { - onBack: () => void; -} - export interface ArticleViewProps { article: ArticleModel; onBack: () => void; diff --git a/src/blog/types/views.ts b/src/blog/types/views.ts index cb0d394..d2c17d3 100644 --- a/src/blog/types/views.ts +++ b/src/blog/types/views.ts @@ -1,9 +1,8 @@ -import {ArticleModel} from "./models"; +import { ArticleModel } from "./models"; export type View = | "home" - | "login" - | "register" + | "auth" | "article" | "editor" | "profile" @@ -17,25 +16,26 @@ export type ViewNode = { export const VIEW_TREE: Record = { home: { parent: null, - children: ["login", "article", "profile", "create"], + children: ["auth", "article", "profile", "create"], }, - login: { + + auth: { parent: "home", - children: ["register"], - }, - register: { - parent: "login", }, + article: { parent: "home", children: ["editor"], }, + editor: { parent: "article", }, + profile: { parent: "home", }, + create: { parent: "home", }, @@ -43,11 +43,15 @@ export const VIEW_TREE: Record = { export const VIEW_URL: Record string> = { home: () => "/", - login: () => "/login", - register: () => "/register", + + auth: () => "/auth", + profile: () => "/profile", + create: () => "/create", + article: (ui) => `/articles/${ui.selectedArticle._id ?? ""}`, + editor: (ui) => `/articles/${ui.selectedArticle._id ?? ""}/edit`, }; diff --git a/src/blog/utils/api.ts b/src/blog/utils/api.ts index e6d29f4..4faf7b9 100644 --- a/src/blog/utils/api.ts +++ b/src/blog/utils/api.ts @@ -1,53 +1,19 @@ -// src/utils/api.ts -import axios from 'axios'; +import { createApiClient } from "../../../auth/src"; const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL; const API_BASE = import.meta.env.VITE_API_BASE_URL; -//------------------------------------------------------ -// COMMON TOKEN ATTACHMENT LOGIC -//------------------------------------------------------ -const attachToken = (config: any) => { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}; +/** + * Auth service client + * - login + * - register + * - me + * - logout + * - introspect + */ +export const auth = createApiClient(AUTH_BASE); -const handleAuthError = (error: any) => { - if (error.response?.status === 401) { - console.warn('Token expired or invalid. Logging out...'); - localStorage.removeItem('token'); - // Optional: eventBus, redirect, logout callback - } - return Promise.reject(error); -}; - -//------------------------------------------------------ -// AUTH SERVICE CLIENT -//------------------------------------------------------ -export const auth = axios.create({ - baseURL: AUTH_BASE, - headers: { - 'Content-Type': 'application/json', - }, -}); - -//------------------------------------------------------ -// BLOG SERVICE CLIENT -//------------------------------------------------------ -export const api = axios.create({ - baseURL: API_BASE, - headers: { - 'Content-Type': 'application/json', - }, -}); - -// Attach token + 401 handling -api.interceptors.request.use(attachToken); -api.interceptors.response.use((res) => res, handleAuthError); - -// Auth service ALSO needs token for /me, /logout, /introspect -auth.interceptors.request.use(attachToken); -auth.interceptors.response.use((res) => res, handleAuthError); +/** + * Main application API (blog, articles, etc.) + */ +export const api = createApiClient(API_BASE); -- 2.49.1 From 5f6ae489fa03a257b276060920345bf275b0316a Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 26 Dec 2025 23:16:55 +0530 Subject: [PATCH 2/5] AuthUser as model --- auth/src/context.tsx | 7 +------ auth/src/models.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 auth/src/models.ts diff --git a/auth/src/context.tsx b/auth/src/context.tsx index 8000314..a915b89 100644 --- a/auth/src/context.tsx +++ b/auth/src/context.tsx @@ -2,12 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from "react"; import axios from "axios"; import { tokenStore } from "./token"; import { attachAuthInterceptors } from "./axios"; - -export interface AuthUser { - _id: string; - username: string; - [key: string]: any; -} +import { AuthUser } from "./models"; interface AuthContextModel { currentUser: AuthUser | null; diff --git a/auth/src/models.ts b/auth/src/models.ts new file mode 100644 index 0000000..fa1c352 --- /dev/null +++ b/auth/src/models.ts @@ -0,0 +1,11 @@ +export interface AuthUser { + // meta fields + _id?: string | null; + created_at: string; + updated_at: string; + + // model fields + username: string; + email: string; + is_active: boolean; +} -- 2.49.1 From bb9c411c92a6a3e011f2c2a1ab1be98a8f6a66d0 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sun, 28 Dec 2025 19:48:43 +0530 Subject: [PATCH 3/5] 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 --- auth/src/{context.tsx => contexts.tsx} | 16 +--- auth/src/index.ts | 3 +- src/blog/Blog.tsx | 8 +- src/blog/components/Profile.tsx | 7 +- src/blog/providers/Author.tsx | 102 +++++++++---------------- src/blog/types/contexts.ts | 4 +- src/blog/types/models.ts | 6 +- src/main.jsx | 16 ++-- 8 files changed, 61 insertions(+), 101 deletions(-) rename auth/src/{context.tsx => contexts.tsx} (83%) 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( - - - - + + + + + + - , + ); -- 2.49.1 From 86101a1b1ca23fa505eb452c203d6d645c4ead14 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sun, 28 Dec 2025 19:57:55 +0530 Subject: [PATCH 4/5] cleanup --- src/blog/providers/Author.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/blog/providers/Author.tsx b/src/blog/providers/Author.tsx index 5392843..3b8b293 100644 --- a/src/blog/providers/Author.tsx +++ b/src/blog/providers/Author.tsx @@ -91,6 +91,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children } else { setCurrentUser(null); setAuthors([]); + setError(null); } }, [authUser]); -- 2.49.1 From 99c2477ae87abaef9a38b20936c78f3a1019e6a6 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sun, 28 Dec 2025 20:13:24 +0530 Subject: [PATCH 5/5] auto login after register --- auth/src/contexts.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/auth/src/contexts.tsx b/auth/src/contexts.tsx index f12ba4a..d2745c9 100644 --- a/auth/src/contexts.tsx +++ b/auth/src/contexts.tsx @@ -53,6 +53,7 @@ export function AuthProvider({ setError(null); await auth.post("/register", { username, password }); + await login(username, password); } catch (e: any) { setError(e.response?.data?.detail ?? "Registration failed"); } finally { -- 2.49.1