Add ACP shell and JWT claims
This commit is contained in:
24
api/src/EventSubscriber/JwtCreatedSubscriber.php
Normal file
24
api/src/EventSubscriber/JwtCreatedSubscriber.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
|
||||||
|
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||||
|
|
||||||
|
#[AsEventListener(event: 'lexik_jwt_authentication.on_jwt_created')]
|
||||||
|
class JwtCreatedSubscriber
|
||||||
|
{
|
||||||
|
public function __invoke(JWTCreatedEvent $event): void
|
||||||
|
{
|
||||||
|
$user = $event->getUser();
|
||||||
|
if (!$user instanceof User) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $event->getData();
|
||||||
|
$payload['user_id'] = $user->getId();
|
||||||
|
$payload['username'] = $user->getUsername();
|
||||||
|
$event->setData($payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,3 +164,30 @@ msgstr "Registrierung läuft..."
|
|||||||
|
|
||||||
msgid "form.create_account"
|
msgid "form.create_account"
|
||||||
msgstr "Konto erstellen"
|
msgstr "Konto erstellen"
|
||||||
|
|
||||||
|
msgid "nav.acp"
|
||||||
|
msgstr "ACP"
|
||||||
|
|
||||||
|
msgid "acp.title"
|
||||||
|
msgstr "Administrationsbereich"
|
||||||
|
|
||||||
|
msgid "acp.no_access"
|
||||||
|
msgstr "Du hast keinen Zugriff auf diesen Bereich."
|
||||||
|
|
||||||
|
msgid "acp.general"
|
||||||
|
msgstr "Allgemein"
|
||||||
|
|
||||||
|
msgid "acp.general_hint"
|
||||||
|
msgstr "Globale Einstellungen und Board-Konfiguration erscheinen hier."
|
||||||
|
|
||||||
|
msgid "acp.forums"
|
||||||
|
msgstr "Foren"
|
||||||
|
|
||||||
|
msgid "acp.forums_hint"
|
||||||
|
msgstr "Kategorien und Foren in einer Baumansicht verwalten."
|
||||||
|
|
||||||
|
msgid "acp.users"
|
||||||
|
msgstr "Benutzer"
|
||||||
|
|
||||||
|
msgid "acp.users_hint"
|
||||||
|
msgstr "Werkzeuge zur Benutzerverwaltung erscheinen hier."
|
||||||
|
|||||||
@@ -164,3 +164,30 @@ msgstr "Registering..."
|
|||||||
|
|
||||||
msgid "form.create_account"
|
msgid "form.create_account"
|
||||||
msgstr "Create account"
|
msgstr "Create account"
|
||||||
|
|
||||||
|
msgid "nav.acp"
|
||||||
|
msgstr "ACP"
|
||||||
|
|
||||||
|
msgid "acp.title"
|
||||||
|
msgstr "Admin control panel"
|
||||||
|
|
||||||
|
msgid "acp.no_access"
|
||||||
|
msgstr "You do not have access to this area."
|
||||||
|
|
||||||
|
msgid "acp.general"
|
||||||
|
msgstr "General"
|
||||||
|
|
||||||
|
msgid "acp.general_hint"
|
||||||
|
msgstr "Global settings and board configuration will appear here."
|
||||||
|
|
||||||
|
msgid "acp.forums"
|
||||||
|
msgstr "Forums"
|
||||||
|
|
||||||
|
msgid "acp.forums_hint"
|
||||||
|
msgstr "Manage categories and forums from a tree view."
|
||||||
|
|
||||||
|
msgid "acp.users"
|
||||||
|
msgstr "Users"
|
||||||
|
|
||||||
|
msgid "acp.users_hint"
|
||||||
|
msgstr "User management tools will appear here."
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import ForumView from './pages/ForumView'
|
|||||||
import ThreadView from './pages/ThreadView'
|
import ThreadView from './pages/ThreadView'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import Register from './pages/Register'
|
import Register from './pages/Register'
|
||||||
|
import Acp from './pages/Acp'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
function Navigation() {
|
function Navigation() {
|
||||||
const { token, email, logout } = useAuth()
|
const { token, email, logout, isAdmin } = useAuth()
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
|
||||||
const handleLanguageChange = (locale) => {
|
const handleLanguageChange = (locale) => {
|
||||||
@@ -29,6 +30,11 @@ function Navigation() {
|
|||||||
<Nav.Link as={Link} to="/">
|
<Nav.Link as={Link} to="/">
|
||||||
{t('nav.forums')}
|
{t('nav.forums')}
|
||||||
</Nav.Link>
|
</Nav.Link>
|
||||||
|
{isAdmin && (
|
||||||
|
<Nav.Link as={Link} to="/acp">
|
||||||
|
{t('nav.acp')}
|
||||||
|
</Nav.Link>
|
||||||
|
)}
|
||||||
{!token && (
|
{!token && (
|
||||||
<>
|
<>
|
||||||
<Nav.Link as={Link} to="/login">
|
<Nav.Link as={Link} to="/login">
|
||||||
@@ -62,6 +68,7 @@ function Navigation() {
|
|||||||
|
|
||||||
function AppShell() {
|
function AppShell() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { isAdmin } = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bb-shell">
|
<div className="bb-shell">
|
||||||
@@ -72,6 +79,7 @@ function AppShell() {
|
|||||||
<Route path="/thread/:id" element={<ThreadView />} />
|
<Route path="/thread/:id" element={<ThreadView />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/acp/*" element={<Acp isAdmin={isAdmin} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<footer className="bb-footer">
|
<footer className="bb-footer">
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
@@ -3,29 +3,75 @@ import { login as apiLogin } from '../api/client'
|
|||||||
|
|
||||||
const AuthContext = createContext(null)
|
const AuthContext = createContext(null)
|
||||||
|
|
||||||
|
function decodeJwt(token) {
|
||||||
|
try {
|
||||||
|
const payload = token.split('.')[1]
|
||||||
|
if (!payload) return null
|
||||||
|
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/')
|
||||||
|
const decoded = atob(normalized)
|
||||||
|
return JSON.parse(decoded)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function AuthProvider({ children }) {
|
export function AuthProvider({ children }) {
|
||||||
const [token, setToken] = useState(() => localStorage.getItem('speedbb_token'))
|
const [token, setToken] = useState(() => localStorage.getItem('speedbb_token'))
|
||||||
const [email, setEmail] = useState(() => localStorage.getItem('speedbb_email'))
|
const [email, setEmail] = useState(() => localStorage.getItem('speedbb_email'))
|
||||||
|
const [userId, setUserId] = useState(() => {
|
||||||
|
const stored = localStorage.getItem('speedbb_user_id')
|
||||||
|
if (stored) return stored
|
||||||
|
const payload = token ? decodeJwt(token) : null
|
||||||
|
return payload?.user_id ? String(payload.user_id) : null
|
||||||
|
})
|
||||||
|
const [roles, setRoles] = useState(() => {
|
||||||
|
const stored = localStorage.getItem('speedbb_roles')
|
||||||
|
if (stored) return JSON.parse(stored)
|
||||||
|
const payload = token ? decodeJwt(token) : null
|
||||||
|
return Array.isArray(payload?.roles) ? payload.roles : []
|
||||||
|
})
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
token,
|
token,
|
||||||
email,
|
email,
|
||||||
|
userId,
|
||||||
|
roles,
|
||||||
|
isAdmin: roles.includes('ROLE_ADMIN'),
|
||||||
async login(emailInput, password) {
|
async login(emailInput, password) {
|
||||||
const data = await apiLogin(emailInput, password)
|
const data = await apiLogin(emailInput, password)
|
||||||
localStorage.setItem('speedbb_token', data.token)
|
localStorage.setItem('speedbb_token', data.token)
|
||||||
localStorage.setItem('speedbb_email', emailInput)
|
localStorage.setItem('speedbb_email', emailInput)
|
||||||
|
const payload = decodeJwt(data.token)
|
||||||
|
if (payload?.user_id) {
|
||||||
|
localStorage.setItem('speedbb_user_id', String(payload.user_id))
|
||||||
|
setUserId(String(payload.user_id))
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('speedbb_user_id')
|
||||||
|
setUserId(null)
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload?.roles)) {
|
||||||
|
localStorage.setItem('speedbb_roles', JSON.stringify(payload.roles))
|
||||||
|
setRoles(payload.roles)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('speedbb_roles')
|
||||||
|
setRoles([])
|
||||||
|
}
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
setEmail(emailInput)
|
setEmail(emailInput)
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
localStorage.removeItem('speedbb_token')
|
localStorage.removeItem('speedbb_token')
|
||||||
localStorage.removeItem('speedbb_email')
|
localStorage.removeItem('speedbb_email')
|
||||||
|
localStorage.removeItem('speedbb_user_id')
|
||||||
|
localStorage.removeItem('speedbb_roles')
|
||||||
setToken(null)
|
setToken(null)
|
||||||
setEmail(null)
|
setEmail(null)
|
||||||
|
setUserId(null)
|
||||||
|
setRoles([])
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[token, email]
|
[token, email, userId, roles]
|
||||||
)
|
)
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||||
|
|||||||
32
frontend/src/pages/Acp.jsx
Normal file
32
frontend/src/pages/Acp.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Container, Tab, Tabs } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function Acp({ isAdmin }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return (
|
||||||
|
<Container className="py-5">
|
||||||
|
<h2 className="mb-3">{t('acp.title')}</h2>
|
||||||
|
<p className="bb-muted">{t('acp.no_access')}</p>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="py-5">
|
||||||
|
<h2 className="mb-4">{t('acp.title')}</h2>
|
||||||
|
<Tabs defaultActiveKey="general" className="mb-3">
|
||||||
|
<Tab eventKey="general" title={t('acp.general')}>
|
||||||
|
<p className="bb-muted">{t('acp.general_hint')}</p>
|
||||||
|
</Tab>
|
||||||
|
<Tab eventKey="forums" title={t('acp.forums')}>
|
||||||
|
<p className="bb-muted">{t('acp.forums_hint')}</p>
|
||||||
|
</Tab>
|
||||||
|
<Tab eventKey="users" title={t('acp.users')}>
|
||||||
|
<p className="bb-muted">{t('acp.users_hint')}</p>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user