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) => (