diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2e2a157..79a6da7 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,12 +1,15 @@
import { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import { AuthProvider } from './context/AuthContext';
import Layout from './components/layout/Layout';
+import RequireAuth from './components/RequireAuth';
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
import CallbackPage from './pages/CallbackPage';
import VideoPage from './pages/VideoPage';
import PlaylistsPage from './pages/PlaylistsPage';
import PlaylistPage from './pages/PlaylistPage';
+import AdminPage from './pages/AdminPage';
type Theme = 'dark' | 'light';
@@ -23,18 +26,23 @@ function App() {
const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
return (
-
-
- }>
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
+
+
+
+ }>
+ } />
+ } />
+ } />
+ } />
+ }>
+ } />
+ } />
+ } />
+
+
+
+
+
);
}
diff --git a/frontend/src/components/RequireAuth.tsx b/frontend/src/components/RequireAuth.tsx
new file mode 100644
index 0000000..48fb982
--- /dev/null
+++ b/frontend/src/components/RequireAuth.tsx
@@ -0,0 +1,15 @@
+import { Navigate, Outlet, useLocation } from 'react-router-dom';
+import { useAuthContext } from '../context/AuthContext';
+
+export default function RequireAuth() {
+ const { user, loading } = useAuthContext();
+ const location = useLocation();
+
+ if (loading) return null;
+
+ if (!user) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx
index ac3dde3..33a74a9 100644
--- a/frontend/src/components/VideoPlayer.tsx
+++ b/frontend/src/components/VideoPlayer.tsx
@@ -20,10 +20,11 @@ export default function VideoPlayer({ storageType, storageKey }: VideoPlayerProp
return ;
}
+ const apiBase = import.meta.env.VITE_API_URL || '/api';
const url =
storageType === 'external'
? storageKey
- : `${import.meta.env.VITE_API_URL}/stream/${storageKey}`;
+ : `${apiBase}/stream/${storageKey}`;
return ;
}
diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx
index 952461f..0720efc 100644
--- a/frontend/src/components/layout/Header.tsx
+++ b/frontend/src/components/layout/Header.tsx
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom';
import { apiFetch } from '../../lib/api';
-import type { User } from '../../hooks/useAuth';
+import type { User } from '../../context/AuthContext';
interface HeaderProps {
theme: 'dark' | 'light';
@@ -39,6 +39,11 @@ export default function Header({ theme, onToggleTheme, user, onLogout }: HeaderP
Playlists
)}
+ {user && (
+
+ admin
+
+ )}
{/* Right — thème + auth */}
diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx
index bc75d83..ca5c22e 100644
--- a/frontend/src/components/layout/Layout.tsx
+++ b/frontend/src/components/layout/Layout.tsx
@@ -1,6 +1,6 @@
import { Outlet } from 'react-router-dom';
import Header from './Header';
-import { useAuth } from '../../hooks/useAuth';
+import { useAuthContext } from '../../context/AuthContext';
interface LayoutProps {
theme: 'dark' | 'light';
@@ -8,7 +8,7 @@ interface LayoutProps {
}
export default function Layout({ theme, onToggleTheme }: LayoutProps) {
- const { user, loading, setUser } = useAuth();
+ const { user, loading, setUser } = useAuthContext();
return (
diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx
new file mode 100644
index 0000000..e8146e3
--- /dev/null
+++ b/frontend/src/context/AuthContext.tsx
@@ -0,0 +1,50 @@
+import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
+import { apiFetch } from '../lib/api';
+
+export interface User {
+ id: string;
+ email: string | null;
+ nickname: string;
+ subscriptionLevel?: number;
+}
+
+interface AuthContextValue {
+ user: User | null;
+ loading: boolean;
+ setUser: (u: User | null) => void;
+}
+
+const AuthContext = createContext
(null);
+
+interface MeResponse {
+ success: boolean;
+ data: { user: User };
+}
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ apiFetch('/auth/me')
+ .then((res) => { if (!cancelled) setUser(res.data.user); })
+ .catch(() => { if (!cancelled) setUser(null); })
+ .finally(() => { if (!cancelled) setLoading(false); });
+
+ return () => { cancelled = true; };
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useAuthContext(): AuthContextValue {
+ const ctx = useContext(AuthContext);
+ if (!ctx) throw new Error('useAuthContext must be used inside AuthProvider');
+ return ctx;
+}
diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx
new file mode 100644
index 0000000..0421e2d
--- /dev/null
+++ b/frontend/src/pages/AdminPage.tsx
@@ -0,0 +1,439 @@
+import { useState, useEffect } from 'react';
+import { apiFetch } from '../lib/api';
+
+// ── Types ────────────────────────────────────────────────────────────────────
+
+interface Video {
+ id: string;
+ title: string;
+ storageType: string;
+ storageKey: string;
+ requiredLevel: number;
+ isPublished: boolean;
+ createdAt: string;
+}
+
+interface Plan {
+ id: string;
+ slug: string;
+ name: string;
+ level: number;
+ priceInCents: number;
+ isActive: boolean;
+}
+
+interface AdminUser {
+ id: string;
+ email: string | null;
+ nickname: string;
+ isActive: boolean;
+ createdAt: string;
+ roles: { id: string; slug: string; name: string }[];
+ activeSubscription: {
+ id: string;
+ status: string;
+ endsAt: string | null;
+ plan: Plan;
+ } | null;
+}
+
+// ── Tabs ─────────────────────────────────────────────────────────────────────
+
+type Tab = 'videos' | 'users' | 'plans';
+
+export default function AdminPage() {
+ const [tab, setTab] = useState('videos');
+
+ return (
+
+
+ {(['videos', 'users', 'plans'] as Tab[]).map((t) => (
+
+ ))}
+
+
+ {tab === 'videos' &&
}
+ {tab === 'users' &&
}
+ {tab === 'plans' &&
}
+
+ );
+}
+
+// ── Videos tab ───────────────────────────────────────────────────────────────
+
+function VideosTab() {
+ const [videos, setVideos] = useState