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"
|
||||
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"
|
||||
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 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
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