Add ACP shell and JWT claims

This commit is contained in:
Micha
2025-12-24 13:29:28 +01:00
parent 193273c843
commit 5ed9d0e1f8
6 changed files with 166 additions and 2 deletions

View 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);
}
}

View File

@@ -164,3 +164,30 @@ msgstr "Registrierung läuft..."
msgid "form.create_account"
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."

View File

@@ -164,3 +164,30 @@ msgstr "Registering..."
msgid "form.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."

View File

@@ -6,10 +6,11 @@ import ForumView from './pages/ForumView'
import ThreadView from './pages/ThreadView'
import Login from './pages/Login'
import Register from './pages/Register'
import Acp from './pages/Acp'
import { useTranslation } from 'react-i18next'
function Navigation() {
const { token, email, logout } = useAuth()
const { token, email, logout, isAdmin } = useAuth()
const { t, i18n } = useTranslation()
const handleLanguageChange = (locale) => {
@@ -29,6 +30,11 @@ function Navigation() {
<Nav.Link as={Link} to="/">
{t('nav.forums')}
</Nav.Link>
{isAdmin && (
<Nav.Link as={Link} to="/acp">
{t('nav.acp')}
</Nav.Link>
)}
{!token && (
<>
<Nav.Link as={Link} to="/login">
@@ -62,6 +68,7 @@ function Navigation() {
function AppShell() {
const { t } = useTranslation()
const { isAdmin } = useAuth()
return (
<div className="bb-shell">
@@ -72,6 +79,7 @@ function AppShell() {
<Route path="/thread/:id" element={<ThreadView />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/acp/*" element={<Acp isAdmin={isAdmin} />} />
</Routes>
<footer className="bb-footer">
<Container>

View File

@@ -3,29 +3,75 @@ import { login as apiLogin } from '../api/client'
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 }) {
const [token, setToken] = useState(() => localStorage.getItem('speedbb_token'))
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(
() => ({
token,
email,
userId,
roles,
isAdmin: roles.includes('ROLE_ADMIN'),
async login(emailInput, password) {
const data = await apiLogin(emailInput, password)
localStorage.setItem('speedbb_token', data.token)
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)
setEmail(emailInput)
},
logout() {
localStorage.removeItem('speedbb_token')
localStorage.removeItem('speedbb_email')
localStorage.removeItem('speedbb_user_id')
localStorage.removeItem('speedbb_roles')
setToken(null)
setEmail(null)
setUserId(null)
setRoles([])
},
}),
[token, email]
[token, email, userId, roles]
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>

View 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>
)
}