diff --git a/app/routes/admin+/index.tsx b/app/routes/admin+/index.tsx new file mode 100644 index 00000000..9303406c --- /dev/null +++ b/app/routes/admin+/index.tsx @@ -0,0 +1,16 @@ +import { type MetaFunction } from '@remix-run/react' +import { PageContentIndex } from '#app/components/templates/index.ts' + +export default function AdminIndexRoute() { + return +} + +export const meta: MetaFunction = () => { + return [ + { title: `Admin | Epic Notes` }, + { + name: 'description', + content: `Admin page for Epic Notes`, + }, + ] +} diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx new file mode 100644 index 00000000..8f071c08 --- /dev/null +++ b/app/routes/admin.tsx @@ -0,0 +1,54 @@ +import { json, type DataFunctionArgs } from '@remix-run/node' +import { Outlet, useLoaderData } from '@remix-run/react' +import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' +import { + Main, + MainContainer, + MainContent, +} from '#app/components/layout/index.ts' +import { PageSidebar } from '#app/components/templates/index.ts' +import { requireAdminUserId } from '#app/utils/auth.server.ts' +import { prisma } from '#app/utils/db.server.ts' +import { invariantResponse } from '#app/utils/misc.tsx' + +export async function loader({ request }: DataFunctionArgs) { + const userId = await requireAdminUserId(request) + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + name: true, + username: true, + image: { select: { id: true } }, + }, + }) + invariantResponse(user, 'User not found', { status: 404 }) + return json({ user }) +} + +export default function AdminRoute() { + const data = useLoaderData() + const { user } = data + return ( +
+ + + + + + +
+ ) +} + +export function ErrorBoundary() { + return ( + ( +

No user with the username "{params.username}" exists

+ ), + }} + /> + ) +} diff --git a/app/utils/auth.server.ts b/app/utils/auth.server.ts index 9fef4756..6d880259 100644 --- a/app/utils/auth.server.ts +++ b/app/utils/auth.server.ts @@ -8,6 +8,7 @@ import { prisma } from './db.server.ts' import { combineHeaders, downloadFile } from './misc.tsx' import { type ProviderUser } from './providers/provider.ts' import { authSessionStorage } from './session.server.ts' +import { redirectWithToast } from './toast.server.ts' export const SESSION_EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30 export const getSessionExpirationDate = () => @@ -63,6 +64,22 @@ export async function requireUserId( return userId } +export async function requireAdminUserId(request: Request) { + const userId = await requireUserId(request) + const user = await prisma.user.findFirst({ + select: { id: true }, + where: { id: userId, roles: { some: { name: 'admin' } } }, + }) + if (!user) { + throw await redirectWithToast(request.headers.get('Referer') ?? '/', { + title: 'Unauthorized', + description: `You do not have permission to access this page.`, + type: 'error', + }) + } + return userId +} + export async function requireAnonymous(request: Request) { const userId = await getUserId(request) if (userId) { diff --git a/playwright.config.ts b/playwright.config.ts index 28b3d59e..d0d0502c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ testDir: './tests/e2e', timeout: 15 * 1000, expect: { - timeout: 5 * 1000, + timeout: 3 * 1000, }, fullyParallel: true, forbidOnly: !!process.env.CI, diff --git a/tests/e2e/admin/admin-utils.ts b/tests/e2e/admin/admin-utils.ts new file mode 100644 index 00000000..08cdb4d9 --- /dev/null +++ b/tests/e2e/admin/admin-utils.ts @@ -0,0 +1,11 @@ +import { type Page } from '@playwright/test' +import { goTo } from '#tests/utils/page-utils.ts' +import { expectUrl } from '#tests/utils/url-utils.ts' + +export async function goToAdminPage(page: Page) { + await goTo(page, '/admin') +} + +export async function expectAdminPage(page: Page) { + await expectUrl({ page, url: '/admin' }) +} diff --git a/tests/e2e/admin/admin.test.ts b/tests/e2e/admin/admin.test.ts new file mode 100644 index 00000000..78e69c17 --- /dev/null +++ b/tests/e2e/admin/admin.test.ts @@ -0,0 +1,24 @@ +import { test } from '#tests/playwright-utils.ts' +import { expectLoginUrl, expectUrl } from '#tests/utils/url-utils.ts' +import { expectAdminPage, goToAdminPage } from './admin-utils.ts' + +test.describe('User cannot view Admin', () => { + test('when not logged in', async ({ page }) => { + await goToAdminPage(page) + await expectLoginUrl({ page, redirectTo: '/admin' }) + }) + + test('when logged in as user', async ({ page, login }) => { + await login() + await goToAdminPage(page) + await expectUrl({ page, url: '/' }) + }) +}) + +test.describe('User can view Admin', () => { + test('when logged in as admin', async ({ page, login }) => { + await login({ roles: ['user', 'admin'] }) + await goToAdminPage(page) + await expectAdminPage(page) + }) +}) diff --git a/tests/e2e/notes/create.test.ts b/tests/e2e/notes/create.test.ts index 361a47f4..1d8796d4 100644 --- a/tests/e2e/notes/create.test.ts +++ b/tests/e2e/notes/create.test.ts @@ -1,6 +1,6 @@ import { test, expect } from '#tests/playwright-utils.ts' import { clickLink, goTo } from '#tests/utils/page-utils.ts' -import { expectURL } from '#tests/utils/url-utils.ts' +import { expectUrl } from '#tests/utils/url-utils.ts' import { createNote, expectCreatedNotePage, @@ -15,7 +15,7 @@ test.describe('Users cannot create notes', () => { test.describe('when not authorized', () => { test('when not logged in', async ({ page, login }) => { await goTo(page, '/users/username/notes/new') - await expectURL({ page, url: /\/login/ }) + await expectUrl({ page, url: /\/login/ }) }) // TODO: what to do when this happens? @@ -23,7 +23,7 @@ test.describe('Users cannot create notes', () => { // const user = await login() // const anotherUser = await login() // await goTo(page, `/users/${anotherUser.username}/notes/new`) - // await expectURL({ page, url: new RegExp(`/users/${user.username}/notes`) }) + // await expectUrl({ page, url: new RegExp(`/users/${user.username}/notes`) }) // }) }) diff --git a/tests/e2e/notes/notes-utils.ts b/tests/e2e/notes/notes-utils.ts index ce09d029..ae353d07 100644 --- a/tests/e2e/notes/notes-utils.ts +++ b/tests/e2e/notes/notes-utils.ts @@ -3,7 +3,7 @@ import { type Page } from '@playwright/test' import { prisma } from '#app/utils/db.server.ts' import { expect } from '#tests/playwright-utils.ts' import { fillSubmitForm, goTo } from '#tests/utils/page-utils.ts' -import { expectURL } from '#tests/utils/url-utils.ts' +import { expectUrl } from '#tests/utils/url-utils.ts' export interface TestNote { title: string @@ -53,17 +53,17 @@ export async function fillAndSubmitNoteForm( export async function expectNotesPage(page: Page, username: string) { const url = `/users/${username}/notes` - await expectURL({ page, url }) + await expectUrl({ page, url }) } export async function expectNewNotePage(page: Page, username: string) { const url = `/users/${username}/notes/new` - await expectURL({ page, url }) + await expectUrl({ page, url }) } export async function expectCreatedNotePage(page: Page, username: string) { const url = new RegExp(`/users/${username}/notes/.*`) - await expectURL({ page, url }) + await expectUrl({ page, url }) } export async function expectNotePage( @@ -72,7 +72,7 @@ export async function expectNotePage( noteId: string, ) { const url = `/users/${username}/notes/${noteId}` - await expectURL({ page, url }) + await expectUrl({ page, url }) } export async function expectNoteEditPage( @@ -81,7 +81,7 @@ export async function expectNoteEditPage( noteId: string, ) { const url = `/users/${username}/notes/${noteId}/edit` - await expectURL({ page, url }) + await expectUrl({ page, url }) } export async function expectFieldInvalid(page: Page, fieldName: string) { diff --git a/tests/e2e/users/users-utils.ts b/tests/e2e/users/users-utils.ts index 9fb49125..0b620d18 100644 --- a/tests/e2e/users/users-utils.ts +++ b/tests/e2e/users/users-utils.ts @@ -9,7 +9,7 @@ import { expectNoLink, goTo, } from '#tests/utils/page-utils.ts' -import { expectURL } from '#tests/utils/url-utils.ts' +import { expectUrl } from '#tests/utils/url-utils.ts' export async function goToUserPage(page: Page, username: string) { await goTo(page, `/users/${username}`) @@ -17,7 +17,7 @@ export async function goToUserPage(page: Page, username: string) { export async function expectUserPage(page: Page, username: string) { const url = `/users/${username}` - await expectURL({ page, url }) + await expectUrl({ page, url }) } async function getUser(username: string) { diff --git a/tests/playwright-utils.ts b/tests/playwright-utils.ts index c342d2b0..f392ca83 100644 --- a/tests/playwright-utils.ts +++ b/tests/playwright-utils.ts @@ -45,6 +45,20 @@ async function getOrInsertUser({ username ??= userData.username password ??= userData.username email ??= userData.email + + // create roles if they don't exist + for (const role of roles) { + const existingRole = await prisma.role.findUnique({ + where: { name: role }, + }) + + if (!existingRole) { + await prisma.role.create({ + data: { name: role }, + }) + } + } + return await prisma.user.create({ select, data: { diff --git a/tests/utils/url-utils.ts b/tests/utils/url-utils.ts index afaa2ac3..610fb30f 100644 --- a/tests/utils/url-utils.ts +++ b/tests/utils/url-utils.ts @@ -5,7 +5,7 @@ interface ExpectURLProps { url: string | RegExp } -export async function expectURL({ page, url }: ExpectURLProps) { +export async function expectUrl({ page, url }: ExpectURLProps) { await expect(page).toHaveURL(url) } @@ -21,6 +21,5 @@ export async function expectLoginUrl({ const expectedUrl = redirectTo ? `/login?redirectTo=${encodeURIComponent(redirectTo)}` : '/login' - await expectURL({ page, url: expectedUrl }) - // await expectURL({ page, url: '/login' }) + await expectUrl({ page, url: expectedUrl }) }