Compare commits
5 Commits
0.3.0
...
86101a1b1c
| Author | SHA1 | Date | |
|---|---|---|---|
| 86101a1b1c | |||
| bb9c411c92 | |||
| 5f6ae489fa | |||
| 2c18c7258b | |||
| 14b43cb3c5 |
@@ -15,7 +15,8 @@ COPY . .
|
|||||||
|
|
||||||
# Build the app
|
# Build the app
|
||||||
ARG VITE_API_BASE_URL
|
ARG VITE_API_BASE_URL
|
||||||
RUN VITE_API_BASE_URL=$VITE_API_BASE_URL npm run build
|
ARG VITE_AUTH_BASE_URL
|
||||||
|
RUN VITE_API_BASE_URL=$VITE_API_BASE_URL VITE_AUTH_BASE_URL=$VITE_AUTH_BASE_URL npm run build
|
||||||
|
|
||||||
# Stage 2: Static file server (BusyBox)
|
# Stage 2: Static file server (BusyBox)
|
||||||
FROM busybox:latest
|
FROM busybox:latest
|
||||||
|
|||||||
11
auth/package.json
Normal file
11
auth/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,49 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Box, TextField, Button, Typography, IconButton, CircularProgress, Link } from '@mui/material';
|
import { Box, TextField, Button, Typography, IconButton, CircularProgress, Link } from '@mui/material';
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
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<void>;
|
||||||
|
register(username: string, password: string): Promise<void>;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
currentUser: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthPage({
|
||||||
|
mode,
|
||||||
onBack,
|
onBack,
|
||||||
onRegister
|
onSwitchMode,
|
||||||
}: LoginProps) {
|
login,
|
||||||
const { login, loading, error, currentUser } = useAuth();
|
register,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
currentUser,
|
||||||
|
}: AuthPageProps) {
|
||||||
|
|
||||||
const [username, setUsername] = React.useState('');
|
const [username, setUsername] = React.useState('');
|
||||||
const [password, setPassword] = React.useState('');
|
const [password, setPassword] = React.useState('');
|
||||||
|
|
||||||
|
const isLogin = mode === "login";
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (isLogin) {
|
||||||
await login(username, password);
|
await login(username, password);
|
||||||
|
} else {
|
||||||
|
await register(username, password);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ✅ Auto-return if already logged in
|
// ✅ Auto-return if already logged in
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (currentUser) onBack();
|
if (currentUser) onBack();
|
||||||
}, [currentUser]);
|
}, [currentUser, onBack]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -39,11 +62,13 @@ export default function Login({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Typography variant="h4" fontWeight="bold" gutterBottom>
|
<Typography variant="h4" fontWeight="bold" gutterBottom>
|
||||||
Sign In
|
{isLogin ? "Sign In" : "Create Account"}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
Please log in to continue
|
{isLogin
|
||||||
|
? "Please log in to continue"
|
||||||
|
: "Create an account to get started"}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
@@ -55,6 +80,7 @@ export default function Login({
|
|||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
required
|
required
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -80,7 +106,13 @@ export default function Login({
|
|||||||
sx={{ mt: 3 }}
|
sx={{ mt: 3 }}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? <CircularProgress size={24} color="inherit" /> : 'Login'}
|
{loading ? (
|
||||||
|
<CircularProgress size={24} color="inherit" />
|
||||||
|
) : isLogin ? (
|
||||||
|
"Login"
|
||||||
|
) : (
|
||||||
|
"Register"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -90,15 +122,15 @@ export default function Login({
|
|||||||
align="center"
|
align="center"
|
||||||
sx={{ mt: 3 }}
|
sx={{ mt: 3 }}
|
||||||
>
|
>
|
||||||
Don’t have an account?{' '}
|
{isLogin ? "Don’t have an account?" : "Already have an account?"}{' '}
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={onRegister}
|
onClick={onSwitchMode}
|
||||||
sx={{ fontWeight: 500 }}
|
sx={{ fontWeight: 500 }}
|
||||||
>
|
>
|
||||||
Register
|
{isLogin ? "Register" : "Login"}
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
32
auth/src/authClient.ts
Normal file
32
auth/src/authClient.ts
Normal file
@@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
35
auth/src/axios.ts
Normal file
35
auth/src/axios.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
96
auth/src/contexts.tsx
Normal file
96
auth/src/contexts.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { tokenStore } from "./token";
|
||||||
|
import { createApiClient } from "./axios";
|
||||||
|
import { AuthUser } from "./models";
|
||||||
|
|
||||||
|
interface AuthContextModel {
|
||||||
|
currentUser: AuthUser | null;
|
||||||
|
token: string | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
login(username: string, password: string): Promise<void>;
|
||||||
|
register(username: string, password: string): Promise<void>;
|
||||||
|
logout(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextModel | undefined>(undefined);
|
||||||
|
|
||||||
|
export function AuthProvider({
|
||||||
|
children,
|
||||||
|
authBaseUrl,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
authBaseUrl: string;
|
||||||
|
}) {
|
||||||
|
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 = createApiClient(authBaseUrl);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
} 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");
|
||||||
|
setCurrentUser({ ...me.data });
|
||||||
|
} catch {
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCurrentUser();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{ currentUser, token, loading, error, login, logout, register }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth(): AuthContextModel {
|
||||||
|
const ctx = useContext(AuthContext);
|
||||||
|
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
6
auth/src/index.ts
Normal file
6
auth/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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"
|
||||||
11
auth/src/models.ts
Normal file
11
auth/src/models.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
0
auth/src/props.ts
Normal file
0
auth/src/props.ts
Normal file
15
auth/src/token.ts
Normal file
15
auth/src/token.ts
Normal file
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aetoskia-blog-app",
|
"name": "aetoskia-blog-app",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aetoskia-blog-app",
|
"name": "aetoskia-blog-app",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -10,34 +10,36 @@ import ArticleView from './components/Article/ArticleView';
|
|||||||
import ArticleEditor from './components/Article/ArticleEditor';
|
import ArticleEditor from './components/Article/ArticleEditor';
|
||||||
import Latest from './components/Latest';
|
import Latest from './components/Latest';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import Login from './components/Login';
|
|
||||||
import Register from './components/Register';
|
|
||||||
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 { useAuth, AuthPage, AuthMode } from '../../auth/src';
|
||||||
|
|
||||||
function HomeView({
|
function HomeView({
|
||||||
currentUser,
|
currentUser,
|
||||||
open_login,
|
open_auth,
|
||||||
open_profile,
|
open_profile,
|
||||||
open_create,
|
open_create,
|
||||||
articles,
|
articles,
|
||||||
openArticle
|
openArticle,
|
||||||
}: any) {
|
}: any) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ display: "flex", justifyContent: "flex-end", mb: 2, gap: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2, gap: 1 }}>
|
||||||
{!currentUser ? (
|
{!currentUser ? (
|
||||||
<Button variant="outlined" onClick={open_login}>Login</Button>
|
<Button variant='outlined' onClick={() => open_auth('login')}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button variant="outlined" onClick={open_profile}>
|
<Button variant='outlined' onClick={open_profile}>
|
||||||
{currentUser.username}
|
{currentUser.username}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" onClick={open_create}>
|
<Button variant='contained' onClick={open_create}>
|
||||||
New Article
|
New Article
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -52,11 +54,13 @@ 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 { currentUser } = useAuth();
|
const auth = useAuth();
|
||||||
|
const { currentUser } = useAuthor();
|
||||||
|
|
||||||
const [ui, setUI] = React.useState({
|
const [ui, setUI] = React.useState({
|
||||||
selectedArticle: null as ArticleModel | null,
|
selectedArticle: null as ArticleModel | null,
|
||||||
view: "home" as View,
|
view: 'home' as View,
|
||||||
|
authMode: 'login' as AuthMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,6 +77,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
setUI({
|
setUI({
|
||||||
selectedArticle: article,
|
selectedArticle: article,
|
||||||
view: 'article',
|
view: 'article',
|
||||||
|
authMode: 'login'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +90,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
} = useViewRouter(setUI);
|
} = useViewRouter(setUI);
|
||||||
|
|
||||||
type RouterContext = {
|
type RouterContext = {
|
||||||
ui: any;
|
ui: typeof ui;
|
||||||
articles: ArticlesModel;
|
articles: ArticlesModel;
|
||||||
currentUser: any;
|
currentUser: any;
|
||||||
openArticle: (article: ArticleModel) => void;
|
openArticle: (article: ArticleModel) => void;
|
||||||
@@ -97,20 +102,27 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
navigationMap?: Record<string, string>;
|
navigationMap?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const VIEW_COMPONENTS: Record<View, ViewComponentEntry<any>> = {
|
const VIEW_COMPONENTS: Record<View, ViewComponentEntry<any>> = {
|
||||||
home: {
|
home: {
|
||||||
component: HomeView,
|
component: HomeView,
|
||||||
},
|
},
|
||||||
|
|
||||||
login: {
|
auth: {
|
||||||
component: Login,
|
component: AuthPage,
|
||||||
navigationMap: {
|
extraProps: ({ ui }) => ({
|
||||||
open_register: 'onRegister',
|
mode: ui.authMode,
|
||||||
},
|
onSwitchMode: () =>
|
||||||
},
|
setUI((prev) => ({
|
||||||
|
...prev,
|
||||||
register: {
|
authMode: prev.authMode === 'login' ? 'register' : 'login',
|
||||||
component: Register,
|
})),
|
||||||
|
login: auth.login,
|
||||||
|
register: auth.register,
|
||||||
|
loading: auth.loading,
|
||||||
|
error: auth.error,
|
||||||
|
currentUser: currentUser,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
profile: {
|
profile: {
|
||||||
@@ -123,6 +135,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
open_editor: 'onEdit',
|
open_editor: 'onEdit',
|
||||||
},
|
},
|
||||||
extraProps: ({ ui, articles }) => ({
|
extraProps: ({ ui, articles }) => ({
|
||||||
|
// @ts-ignore
|
||||||
article: articles.readById(ui.selectedArticle._id),
|
article: articles.readById(ui.selectedArticle._id),
|
||||||
}) satisfies Partial<ArticleViewProps>,
|
}) satisfies Partial<ArticleViewProps>,
|
||||||
},
|
},
|
||||||
@@ -130,7 +143,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
editor: {
|
editor: {
|
||||||
component: ArticleEditor,
|
component: ArticleEditor,
|
||||||
extraProps: ({ ui, articles }) => ({
|
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<ArticleEditorProps>,
|
}) satisfies Partial<ArticleEditorProps>,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -147,10 +160,15 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
const navigationMap= entry['navigationMap'] || {}
|
const navigationMap= entry['navigationMap'] || {}
|
||||||
const ViewComponent = entry.component;
|
const ViewComponent = entry.component;
|
||||||
|
|
||||||
const childNav = navigateToChildren(
|
const childNav = {
|
||||||
ui.view,
|
...navigateToChildren(ui.view, navigationMap),
|
||||||
navigationMap
|
open_auth: (mode: AuthMode = 'login') =>
|
||||||
);
|
setUI((prev) => ({
|
||||||
|
...prev,
|
||||||
|
view: 'auth',
|
||||||
|
authMode: mode,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const ctx: RouterContext = {
|
const ctx: RouterContext = {
|
||||||
ui,
|
ui,
|
||||||
@@ -234,7 +252,7 @@ export default function Blog(props: { disableCustomTheme?: boolean }) {
|
|||||||
|
|
||||||
{ui.view === 'home' && (
|
{ui.view === 'home' && (
|
||||||
<Box
|
<Box
|
||||||
component="footer"
|
component='footer'
|
||||||
sx={{
|
sx={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
|||||||
@@ -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,113 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Box, TextField, Button, Typography, IconButton, CircularProgress, Alert, } from '@mui/material';
|
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
|
||||||
import { useAuth } from '../providers/Author';
|
|
||||||
import { RegisterProps } from '../types/props';
|
|
||||||
|
|
||||||
export default function Register({
|
|
||||||
onBack
|
|
||||||
}: RegisterProps) {
|
|
||||||
const { register, loading, error, currentUser } = useAuth();
|
|
||||||
const [username, setUsername] = React.useState('');
|
|
||||||
const [password1, setPassword1] = React.useState('');
|
|
||||||
const [password2, setPassword2] = React.useState('');
|
|
||||||
const [localError, setLocalError] = React.useState<string | null>(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 (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
maxWidth: 400,
|
|
||||||
mx: 'auto',
|
|
||||||
mt: 8,
|
|
||||||
p: 4,
|
|
||||||
borderRadius: 3,
|
|
||||||
boxShadow: 3,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton onClick={onBack} sx={{ mb: 2 }}>
|
|
||||||
<ArrowBackRoundedIcon />
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<Typography variant="h4" fontWeight="bold" gutterBottom>
|
|
||||||
Sign Up
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
||||||
Please sign up to continue
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Username"
|
|
||||||
type="username"
|
|
||||||
margin="normal"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
margin="normal"
|
|
||||||
value={password1}
|
|
||||||
onChange={(e) => setPassword1(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
margin="normal"
|
|
||||||
value={password2}
|
|
||||||
onChange={(e) => setPassword2(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(localError || error) && (
|
|
||||||
<Alert severity="error" sx={{ mt: 2 }}>
|
|
||||||
{localError || error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
sx={{ mt: 3 }}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? <CircularProgress size={24} color="inherit" /> : 'Register'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { createContext, useState, useContext, useEffect } from 'react';
|
import React, { createContext, useState, useContext, useEffect } from "react";
|
||||||
import { api } from '../utils/api';
|
import { api } from "../utils/api";
|
||||||
import {
|
import {
|
||||||
ArticleModel,
|
ArticleModel,
|
||||||
ArticlesModel,
|
ArticlesModel,
|
||||||
createArticlesModelObject,
|
createArticlesModelObject,
|
||||||
} from '../types/models';
|
} from "../types/models";
|
||||||
import { ArticleContextModel } from '../types/contexts';
|
import { ArticleContextModel } from "../types/contexts";
|
||||||
import { useAuth } from './Author';
|
import { useAuth } from "./Author";
|
||||||
|
|
||||||
const ArticleContext = createContext<ArticleContextModel | undefined>(undefined);
|
const ArticleContext = createContext<ArticleContextModel | undefined>(undefined);
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
|||||||
const [articles, setArticles] = useState<ArticlesModel>(createArticlesModelObject());
|
const [articles, setArticles] = useState<ArticlesModel>(createArticlesModelObject());
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { token, currentUser } = useAuth();
|
const { currentUser } = useAuth();
|
||||||
|
|
||||||
/** 🔹 Author IDs must be strings for API, so we normalize here */
|
/** 🔹 Author IDs must be strings for API, so we normalize here */
|
||||||
const normalizeArticleForApi = (article: Partial<ArticleModel>) => {
|
const normalizeArticleForApi = (article: Partial<ArticleModel>) => {
|
||||||
@@ -109,14 +109,8 @@ export const ArticleProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
|||||||
|
|
||||||
/** 🔹 Auto-fetch articles whenever user logs in/out */
|
/** 🔹 Auto-fetch articles whenever user logs in/out */
|
||||||
useEffect(() => {
|
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();
|
fetchArticles();
|
||||||
}
|
}, [currentUser]); // refetch on login / logout
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ArticleContext.Provider value={{
|
<ArticleContext.Provider value={{
|
||||||
|
|||||||
@@ -1,70 +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 { 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 [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
const [loading, setLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState<boolean>(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, 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) */
|
/** 🔹 Fetch all authors (JWT handled by api interceptor) */
|
||||||
const refreshAuthors = async () => {
|
const refreshAuthors = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -102,39 +82,27 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 🔹 Auto-load current user if token exists */
|
/**
|
||||||
const fetchCurrentUser = async () => {
|
* React strictly to auth lifecycle
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (token) fetchCurrentUser();
|
if (authUser) {
|
||||||
}, [token]);
|
hydrateCurrentUser();
|
||||||
|
} else {
|
||||||
|
setCurrentUser(null);
|
||||||
|
setAuthors([]);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}, [authUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
currentUser,
|
currentUser,
|
||||||
authors,
|
authors,
|
||||||
token,
|
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
register,
|
|
||||||
refreshAuthors,
|
refreshAuthors,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -16,12 +16,9 @@ export interface ArticleContextModel {
|
|||||||
export interface AuthContextModel {
|
export interface AuthContextModel {
|
||||||
currentUser: AuthorModel | null;
|
currentUser: AuthorModel | null;
|
||||||
authors: AuthorModel[];
|
authors: AuthorModel[];
|
||||||
token: string | null;
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ export interface LatestProps {
|
|||||||
onLoadMore?: (offset: number, limit: number) => Promise<void>; // optional async callback
|
onLoadMore?: (offset: number, limit: number) => Promise<void>; // optional async callback
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginProps {
|
|
||||||
onBack: () => void;
|
|
||||||
onRegister: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MainContentProps {
|
export interface MainContentProps {
|
||||||
articles: ArticlesModel;
|
articles: ArticlesModel;
|
||||||
onSelectArticle: (index: ArticleModel) => void;
|
onSelectArticle: (index: ArticleModel) => void;
|
||||||
@@ -23,10 +18,6 @@ export interface ProfileProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterProps {
|
|
||||||
onBack: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ArticleViewProps {
|
export interface ArticleViewProps {
|
||||||
article: ArticleModel;
|
article: ArticleModel;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import {ArticleModel} from "./models";
|
|||||||
|
|
||||||
export type View =
|
export type View =
|
||||||
| "home"
|
| "home"
|
||||||
| "login"
|
| "auth"
|
||||||
| "register"
|
|
||||||
| "article"
|
| "article"
|
||||||
| "editor"
|
| "editor"
|
||||||
| "profile"
|
| "profile"
|
||||||
@@ -17,25 +16,26 @@ export type ViewNode = {
|
|||||||
export const VIEW_TREE: Record<View, ViewNode> = {
|
export const VIEW_TREE: Record<View, ViewNode> = {
|
||||||
home: {
|
home: {
|
||||||
parent: null,
|
parent: null,
|
||||||
children: ["login", "article", "profile", "create"],
|
children: ["auth", "article", "profile", "create"],
|
||||||
},
|
},
|
||||||
login: {
|
|
||||||
|
auth: {
|
||||||
parent: "home",
|
parent: "home",
|
||||||
children: ["register"],
|
|
||||||
},
|
|
||||||
register: {
|
|
||||||
parent: "login",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
article: {
|
article: {
|
||||||
parent: "home",
|
parent: "home",
|
||||||
children: ["editor"],
|
children: ["editor"],
|
||||||
},
|
},
|
||||||
|
|
||||||
editor: {
|
editor: {
|
||||||
parent: "article",
|
parent: "article",
|
||||||
},
|
},
|
||||||
|
|
||||||
profile: {
|
profile: {
|
||||||
parent: "home",
|
parent: "home",
|
||||||
},
|
},
|
||||||
|
|
||||||
create: {
|
create: {
|
||||||
parent: "home",
|
parent: "home",
|
||||||
},
|
},
|
||||||
@@ -43,11 +43,15 @@ export const VIEW_TREE: Record<View, ViewNode> = {
|
|||||||
|
|
||||||
export const VIEW_URL: Record<View, (ui?: any) => string> = {
|
export const VIEW_URL: Record<View, (ui?: any) => string> = {
|
||||||
home: () => "/",
|
home: () => "/",
|
||||||
login: () => "/login",
|
|
||||||
register: () => "/register",
|
auth: () => "/auth",
|
||||||
|
|
||||||
profile: () => "/profile",
|
profile: () => "/profile",
|
||||||
|
|
||||||
create: () => "/create",
|
create: () => "/create",
|
||||||
|
|
||||||
article: (ui) => `/articles/${ui.selectedArticle._id ?? ""}`,
|
article: (ui) => `/articles/${ui.selectedArticle._id ?? ""}`,
|
||||||
|
|
||||||
editor: (ui) => `/articles/${ui.selectedArticle._id ?? ""}/edit`,
|
editor: (ui) => `/articles/${ui.selectedArticle._id ?? ""}/edit`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,19 @@
|
|||||||
// src/utils/api.ts
|
import { createApiClient } from "../../../auth/src";
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL;
|
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL;
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
//------------------------------------------------------
|
/**
|
||||||
// COMMON TOKEN ATTACHMENT LOGIC
|
* Auth service client
|
||||||
//------------------------------------------------------
|
* - login
|
||||||
const attachToken = (config: any) => {
|
* - register
|
||||||
const token = localStorage.getItem('token');
|
* - me
|
||||||
if (token) {
|
* - logout
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
* - introspect
|
||||||
}
|
*/
|
||||||
return config;
|
export const auth = createApiClient(AUTH_BASE);
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuthError = (error: any) => {
|
/**
|
||||||
if (error.response?.status === 401) {
|
* Main application API (blog, articles, etc.)
|
||||||
console.warn('Token expired or invalid. Logging out...');
|
*/
|
||||||
localStorage.removeItem('token');
|
export const api = createApiClient(API_BASE);
|
||||||
// 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);
|
|
||||||
|
|||||||
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