From 61fc8dc5ba1bee9ea4b08558b80c18bcdd6b8791 Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Sat, 6 Dec 2025 18:12:06 +0200 Subject: [PATCH] Harden auth/role enforcement for admin and listings --- PROGRESS.md | 1 + app/admin/pending/page.tsx | 25 ++++++++++------ app/admin/users/page.tsx | 21 +++++++++---- app/api/admin/listings/approve/route.ts | 3 ++ app/api/admin/pending/count/route.ts | 3 ++ app/api/admin/pending/route.ts | 3 ++ app/api/admin/users/approve/route.ts | 3 ++ app/api/admin/users/reject/route.ts | 3 ++ app/api/admin/users/remove/route.ts | 3 ++ app/api/admin/users/role/route.ts | 3 ++ app/api/admin/users/route.ts | 3 ++ app/api/listings/remove/route.ts | 2 +- lib/jwt.ts | 16 ++++++++-- middleware.ts | 40 +++++++++++++++++++++++++ 14 files changed, 112 insertions(+), 17 deletions(-) create mode 100644 middleware.ts diff --git a/PROGRESS.md b/PROGRESS.md index 77160ba..28eaeec 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -28,6 +28,7 @@ # Lomavuokraus app progress (Nov 24) - New testing DB (`lomavuokraus_testing`) holds the previous staging/prod data; the main `lomavuokraus` DB was recreated clean with only the seeded admin user. Migration history was copied, and a schema snapshot lives at `docs/db-schema.sql`. - Testing environment wiring added: dedicated namespace (`lomavuokraus-test`), deploy wrapper (`deploy/deploy-test.sh`), API host support, and a DNS updater for `test.lomavuokraus.fi` / `apitest.lomavuokraus.fi`. +- Access control tightened: middleware now gates admin routes, admin-only pages check session/role, API handlers return proper 401/403, and listing removal is limited to owners/admins (no more moderator overrides). - Backend/data: Added Prisma models (User/Listing/ListingTranslation/ListingImage), seed script creates sample listing; DB on Hetzner VM `46.62.203.202`, staging secrets set in `lomavuokraus-web-secrets`. - Auth: Register/login/verify flows; session cookie (`session_token`), NavBar shows email+role badge. Roles: USER, ADMIN, USER_MODERATOR (approve users), LISTING_MODERATOR (approve listings). Admin can change roles at `/admin/users`. - Listing flow: create listing (session required), pending/published with admin/moderator approvals; pages for “My listings,” “New listing,” “Profile.” Quick actions tile removed; all actions in navbar. diff --git a/app/admin/pending/page.tsx b/app/admin/pending/page.tsx index d7360ec..bfb3e8d 100644 --- a/app/admin/pending/page.tsx +++ b/app/admin/pending/page.tsx @@ -10,6 +10,7 @@ export default function PendingAdminPage() { const { t } = useI18n(); const [pendingUsers, setPendingUsers] = useState([]); const [pendingListings, setPendingListings] = useState([]); + const [role, setRole] = useState(null); const [message, setMessage] = useState(null); const [error, setError] = useState(null); @@ -23,6 +24,7 @@ export default function PendingAdminPage() { setError(data.error || 'Failed to load'); return; } + setRole(data.role ?? null); setPendingUsers(data.users ?? []); setPendingListings(data.listings ?? []); } catch (e) { @@ -34,11 +36,16 @@ export default function PendingAdminPage() { fetch('/api/auth/me', { cache: 'no-store' }) .then((res) => res.json()) .then((data) => { - if (data.user?.role === 'ADMIN') { - loadPending(); - } else { + setRole(data.user?.role ?? null); + if (!data.user?.role) { setError(t('adminRequired')); + return; } + if (['ADMIN', 'USER_MODERATOR', 'LISTING_MODERATOR'].includes(data.user.role)) { + loadPending(); + return; + } + setError(t('adminRequired')); }) .catch(() => setError(t('adminRequired'))); }, [t]); @@ -117,13 +124,13 @@ export default function PendingAdminPage() { {u.email} — {t('statusLabel')}: {u.status} — {t('verifiedLabel')}: {u.emailVerifiedAt ? t('yes') : t('no')}
- - -
@@ -147,13 +154,13 @@ export default function PendingAdminPage() { {t('slugsLabel')}: {l.translations.map((t) => `${t.slug} (${t.locale})`).join(', ')}
- - -
diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index 8e11aa6..113d5e2 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -18,6 +18,7 @@ const roleOptions = ['USER', 'USER_MODERATOR', 'LISTING_MODERATOR', 'ADMIN']; export default function AdminUsersPage() { const { t } = useI18n(); const [users, setUsers] = useState([]); + const [role, setRole] = useState(null); const [error, setError] = useState(null); const [message, setMessage] = useState(null); const [loading, setLoading] = useState(false); @@ -38,7 +39,17 @@ export default function AdminUsersPage() { } useEffect(() => { - load(); + fetch('/api/auth/me', { cache: 'no-store' }) + .then((res) => res.json()) + .then((data) => { + setRole(data.user?.role ?? null); + if (data.user?.role === 'ADMIN') { + load(); + } else { + setError(t('adminRequired')); + } + }) + .catch(() => setError(t('adminRequired'))); }, []); async function setRole(userId: string, role: string) { @@ -161,7 +172,7 @@ export default function AdminUsersPage() { {u.email} - setRole(u.id, e.target.value)} disabled={loading || role !== 'ADMIN'}> {roleOptions.map((r) => (