## Summary
Refactored the authentication flow to correctly separate traffic between the
Auth service and Blog service. Added post-registration author creation and
switched all `/auth/*` calls to the dedicated `auth` Axios client.
## Changes
### AuthProvider
- Replaced `api.post('/auth/register')` with `auth.post('/register')`
- Replaced `api.post('/auth/login')` with `auth.post('/login')`
- Added automatic author creation after user registration (`POST /authors`)
- Switched user identity lookup from `api.get('/auth/me')` to `auth.get('/me')`
- Replaced `/authors/{id}` lookup with `/authors/me`
- Updated imports to use `{ api, auth }`
### Axios Client Layer
- Introduced a new `auth` Axios instance using `VITE_AUTH_BASE_URL`
- Added shared token attachment and 401 handling logic
- Applied interceptors to both `auth` and `api` clients
- Removed inline auth logic from `api.ts`
### Types
- Added `VITE_AUTH_BASE_URL` to `vite-env.d.ts`
## Impact
- Correctly routes authentication traffic to the Auth microservice
- Ensures an Author document is created automatically after registration
- Simplifies identity loading via `/authors/me`
- Improves token handling consistency across both services
152 lines
4.1 KiB
TypeScript
152 lines
4.1 KiB
TypeScript
import React, { createContext, useState, useEffect, useContext } from 'react';
|
|
import { api, auth } from '../utils/api';
|
|
import { AuthorModel } from '../types/models';
|
|
import { AuthContextModel } from '../types/contexts';
|
|
|
|
const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
|
|
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const [currentUser, setCurrentUser] = useState<AuthorModel | null>(null);
|
|
const [authors, setAuthors] = useState<AuthorModel[]>([]);
|
|
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
/** 🔹 Register new user */
|
|
const register = async (username: string, password: string) => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const res = await auth.post('/register', { username, password });
|
|
|
|
// auto-login
|
|
// await login(username, password);
|
|
|
|
// 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');
|
|
} 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, user } = res.data;
|
|
|
|
if (access_token) {
|
|
localStorage.setItem('token', access_token);
|
|
setToken(access_token);
|
|
setCurrentUser(user);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Login failed:', err);
|
|
setError(err.response?.data?.detail || 'Invalid credentials');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
/** 🔹 Logout and clear everything */
|
|
const logout = () => {
|
|
localStorage.removeItem('token');
|
|
setToken(null);
|
|
setCurrentUser(null);
|
|
setAuthors([]);
|
|
};
|
|
|
|
/** 🔹 Fetch all authors (JWT handled by api interceptor) */
|
|
const refreshAuthors = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const res = await api.get<AuthorModel[]>('/authors');
|
|
setAuthors(res.data);
|
|
} catch (err: any) {
|
|
console.error('Failed to fetch authors:', err);
|
|
setError(err.response?.data?.detail || 'Failed to fetch authors');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
/** 🔹 Update current user (full model) */
|
|
const updateProfile = async (userData: AuthorModel) => {
|
|
if (!userData._id) {
|
|
console.error('updateProfile called without _id');
|
|
return;
|
|
}
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const res = await api.put<AuthorModel>(`/authors/${userData._id}`, userData);
|
|
setCurrentUser(res.data);
|
|
return res.data;
|
|
} catch (err: any) {
|
|
console.error('Profile update failed:', err);
|
|
setError(err.response?.data?.detail || 'Failed to update profile');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
/** 🔹 Auto-load current user if token exists */
|
|
const fetchCurrentUser = async () => {
|
|
if (!token) return;
|
|
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(() => {
|
|
if (token) fetchCurrentUser();
|
|
}, [token]);
|
|
|
|
return (
|
|
<AuthContext.Provider
|
|
value={{
|
|
currentUser,
|
|
authors,
|
|
token,
|
|
loading,
|
|
error,
|
|
login,
|
|
logout,
|
|
register,
|
|
refreshAuthors,
|
|
updateProfile,
|
|
}}
|
|
>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useAuth = (): AuthContextModel => {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
|
|
return ctx;
|
|
};
|