Auth Package Extraction And Auth Flow Refactor #2

Merged
aetos merged 5 commits from auth-package into main 2025-12-28 14:47:38 +00:00
8 changed files with 61 additions and 101 deletions
Showing only changes of commit bb9c411c92 - Show all commits

View File

@@ -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<AuthContextModel | undefined>(undefined);
export function AuthProvider({
children,
authBaseUrl,
apiClient,
}: {
children: React.ReactNode;
authBaseUrl: string;
apiClient: any; // your domain api client
}) {
const [currentUser, setCurrentUser] = useState<AuthUser | null>(null);
const [token, setToken] = useState<string | null>(tokenStore.get());
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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();
}

View File

@@ -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"

View File

@@ -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,
}),
},

View File

@@ -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 || '',

View File

@@ -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<AuthContextModel | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { currentUser: authUser, logout } = useBaseAuth();
const [currentUser, setCurrentUser] = useState<AuthorModel | null>(null);
const [authors, setAuthors] = useState<AuthorModel[]>([]);
const [loading, setLoading] = useState(false);
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 {
setLoading(true);
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
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<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 */
/**
* React strictly to auth lifecycle
*/
useEffect(() => {
if (tokenStore.get()) {
fetchCurrentUser();
if (authUser) {
hydrateCurrentUser();
} else {
setCurrentUser(null);
setAuthors([]);
}
}, []);
}, [authUser]);
return (
<AuthContext.Provider
@@ -133,9 +101,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
authors,
loading,
error,
login,
logout,
register,
refreshAuthors,
updateProfile,
}}

View File

@@ -18,9 +18,7 @@ export interface AuthContextModel {
authors: AuthorModel[];
loading: boolean;
error: string | null;
login: (username: string, password: string) => Promise<void>;
register: (username: string, password: string) => Promise<void>;
logout: () => void;
refreshAuthors: () => Promise<void>;
updateProfile: (user: AuthorModel) => Promise<AuthorModel | void>;
}

View File

@@ -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 {

View File

@@ -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(
<UploadProvider>
<AuthProvider>
<ArticleProvider>
<Blog />
</ArticleProvider>
<AuthProvider authBaseUrl={AUTH_BASE}>
<AuthorProvider>
<ArticleProvider>
<Blog />
</ArticleProvider>
</AuthorProvider>
</AuthProvider>
</UploadProvider>,
</UploadProvider>
);