Add admin user list view for simon@kuehn.de
- ADMIN_EMAIL env var controls who has admin access - GET /api/admin/users returns all users (id, email, username, registered); returns 403 for non-admins - GET /api/me now includes is_admin flag - Menu shows "Nutzer/Users/Użytkownicy" button for admins that opens a table with name, email, and registration date for all users Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b537066a19
commit
eea6119e36
6 changed files with 1095 additions and 7 deletions
1
.env
1
.env
|
|
@ -15,4 +15,5 @@ MAILER_FROM=noreply@example.com
|
|||
###> app ###
|
||||
APP_URL=http://localhost
|
||||
DEFAULT_URI=http://localhost/dd
|
||||
ADMIN_EMAIL=simon@kuehn.de
|
||||
###< app ###
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
var TODAY = new Date(); TODAY.setHours(0,0,0,0);
|
||||
var goals = [], prefs, selDay = {}, addAmt = {}, renamingId = null, renameVal = '', collapsed = {};
|
||||
var userName = '';
|
||||
var userName = '', isAdmin = false;
|
||||
|
||||
var STRINGS = {
|
||||
de: {
|
||||
|
|
@ -57,6 +57,7 @@ var STRINGS = {
|
|||
linkLabel:'Link',doneLabel:'gemacht',ofLabel:'von',
|
||||
gestern:'Gestern',heute:'Heute',
|
||||
expiresAt:'läuft ab:',acceptedBy:'→',
|
||||
adminLabel:'Nutzer',adminColName:'Name',adminColEmail:'E-Mail',adminColRegistered:'Registriert',
|
||||
},
|
||||
en: {
|
||||
hint:'Menu → "Add to Home Screen" for app icon',
|
||||
|
|
@ -112,6 +113,7 @@ var STRINGS = {
|
|||
linkLabel:'Link',doneLabel:'done',ofLabel:'of',
|
||||
gestern:'Yesterday',heute:'Today',
|
||||
expiresAt:'expires:',acceptedBy:'→',
|
||||
adminLabel:'Users',adminColName:'Name',adminColEmail:'Email',adminColRegistered:'Registered',
|
||||
},
|
||||
pl: {
|
||||
hint:'Menu → "Dodaj do ekranu głównego" aby zainstalować',
|
||||
|
|
@ -167,6 +169,7 @@ var STRINGS = {
|
|||
linkLabel:'Link',doneLabel:'wykonano',ofLabel:'z',
|
||||
gestern:'Wczoraj',heute:'Dziś',
|
||||
expiresAt:'wygasa:',acceptedBy:'→',
|
||||
adminLabel:'Użytkownicy',adminColName:'Nazwa',adminColEmail:'E-mail',adminColRegistered:'Rejestracja',
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -534,6 +537,9 @@ function openData(){
|
|||
api('POST','logout').then(function(){ goals=[]; closeOv(); render(); showLogin(); });
|
||||
};
|
||||
|
||||
var adminBtn=c.querySelector('.dm-admin');
|
||||
if(isAdmin){ adminBtn.style.display=''; adminBtn.onclick=function(){ closeOv(); openAdmin(); }; }
|
||||
|
||||
c.querySelector('.dm-inv').onclick=function(){
|
||||
var ic=tpl('tpl-invite-form');
|
||||
showSheet(ic,true);
|
||||
|
|
@ -633,6 +639,29 @@ function openData(){
|
|||
};
|
||||
}
|
||||
|
||||
// ── Admin ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function openAdmin(){
|
||||
api('GET','admin/users').then(function(rows){
|
||||
var c=tpl('tpl-admin-users');
|
||||
var body=c.querySelector('.au-body');
|
||||
rows.forEach(function(u){
|
||||
var row=document.createElement('tr');
|
||||
row.style.borderBottom='1px solid var(--border)';
|
||||
var name=u.username||'—';
|
||||
var date=new Date(u.registered*1000).toLocaleDateString(ldoc(),{day:'numeric',month:'short',year:'numeric'});
|
||||
row.innerHTML='<td style="padding:7px 12px">'+escHtml(name)+'</td>'
|
||||
+'<td style="padding:7px 12px;color:var(--text2)">'+escHtml(u.email)+'</td>'
|
||||
+'<td style="padding:7px 12px;color:var(--text2);font-size:.85em">'+date+'</td>';
|
||||
body.appendChild(row);
|
||||
});
|
||||
showSheet(c,true);
|
||||
c.querySelector('.au-close').onclick=closeOv;
|
||||
}).catch(function(){ showToast(tr('errLoad')); });
|
||||
}
|
||||
|
||||
function escHtml(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
// ── Card-Bausteine ────────────────────────────────────────────────────────────
|
||||
|
||||
function buildNameWrap(g){
|
||||
|
|
@ -895,7 +924,7 @@ if(resetSelector&&resetToken){
|
|||
applyLocale(null); render(); showResetPassword(resetSelector,resetToken);
|
||||
} else {
|
||||
api('GET','me')
|
||||
.then(function(r){ userName=r.name||''; applyLocale(r.locale); updateHeader(); return loadGoals(); })
|
||||
.then(function(r){ userName=r.name||''; isAdmin=r.is_admin||false; applyLocale(r.locale); updateHeader(); return loadGoals(); })
|
||||
.then(function(g){ goals=g; render(); })
|
||||
.catch(function(){
|
||||
applyLocale(null); render();
|
||||
|
|
|
|||
37
src/Controller/AdminController.php
Normal file
37
src/Controller/AdminController.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/api/admin')]
|
||||
class AdminController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
#[Route('/users', methods: ['GET'])]
|
||||
public function users(): JsonResponse
|
||||
{
|
||||
$user = $this->getUser();
|
||||
if (!$user instanceof User) {
|
||||
return new JsonResponse(['error' => 'Unauthorized'], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if ($user->getEmail() !== ($_ENV['ADMIN_EMAIL'] ?? '')) {
|
||||
return new JsonResponse(['error' => 'Forbidden'], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$rows = $this->em->getConnection()->fetchAllAssociative(
|
||||
'SELECT id, email, username, registered FROM users ORDER BY registered ASC'
|
||||
);
|
||||
|
||||
return new JsonResponse($rows);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,11 +34,12 @@ class AuthController extends AbstractController
|
|||
return new JsonResponse(['ok' => false], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
return new JsonResponse([
|
||||
'ok' => true,
|
||||
'email' => $user->getEmail(),
|
||||
'id' => $user->getId(),
|
||||
'name' => $user->getUsername() ?? '',
|
||||
'locale' => $user->getLocale(),
|
||||
'ok' => true,
|
||||
'email' => $user->getEmail(),
|
||||
'id' => $user->getId(),
|
||||
'name' => $user->getUsername() ?? '',
|
||||
'locale' => $user->getLocale(),
|
||||
'is_admin' => $user->getEmail() === ($_ENV['ADMIN_EMAIL'] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@
|
|||
<div class="ddiv"></div>
|
||||
<button class="dbtn dm-inv"><span class="dico">🔗</span><span class="dlbl"><span data-t="inviteLabel"></span><span class="dsub" data-t="inviteSub"></span></span></button>
|
||||
<button class="dbtn dm-invlist"><span class="dico">📋</span><span class="dlbl"><span data-t="inviteListLabel"></span><span class="dsub" data-t="inviteListSub"></span></span></button>
|
||||
<button class="dbtn dm-admin" style="display:none"><span class="dico">👥</span><span class="dlbl" data-t="adminLabel"></span></button>
|
||||
<div class="ddiv"></div>
|
||||
<button class="dbtn dm-name"><span class="dico">✏️</span><span class="dlbl" data-t="changeName"></span></button>
|
||||
<button class="dbtn dm-cpw"><span class="dico">🔑</span><span class="dlbl" data-t="changePw"></span></button>
|
||||
|
|
@ -336,6 +337,25 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-admin-users">
|
||||
<div>
|
||||
<div class="stitle" data-t="adminLabel"></div>
|
||||
<div class="dpanel-body" style="overflow-x:auto;padding:0">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||||
<thead>
|
||||
<tr style="border-bottom:1px solid var(--border)">
|
||||
<th style="padding:8px 12px;text-align:left;font-weight:600" data-t="adminColName"></th>
|
||||
<th style="padding:8px 12px;text-align:left;font-weight:600" data-t="adminColEmail"></th>
|
||||
<th style="padding:8px 12px;text-align:left;font-weight:600;color:var(--text2)" data-t="adminColRegistered"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="au-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="factions"><button class="btn-c au-close" data-t="close"></button></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="app.js?v={{ jsv }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in a new issue