From 3ba8cc125d925a587c71bc8707d9f9c483c1d1da Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:19:38 +0200 Subject: [PATCH 01/23] restrict certain routes attempt --- src/middleware.ts | 53 ++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index a7ea89e..37d68ee 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,37 +1,42 @@ -'use server' -import 'server-only' -import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' +import { clerkMiddleware, createRouteMatcher, auth } from '@clerk/nextjs/server' import { NextResponse } from 'next/server' -const isMemberRoute = createRouteMatcher(['/fix-me/member(.*)']) -const isResearcherRoute = createRouteMatcher(['/fix-me/researcher(.*)']) -// Clerk middleware reference -// https://clerk.com/docs/references/nextjs/clerk-middleware +const isMemberRoute = createRouteMatcher(['/member/(.*)']) +const isResearcherRoute = createRouteMatcher(['/researcher/(.*)']) -export default clerkMiddleware((auth, req) => { - // Do not allow and redirect certain paths when logged in (e.g. password resets, signup) +const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' + +export default clerkMiddleware(async (auth, req) => { + const { userId, organizations } = auth + + // Redirect logged-in users away from auth pages if (req.nextUrl.pathname.startsWith('/reset-password') || req.nextUrl.pathname.startsWith('/signup')) { - const { userId } = auth() if (userId) { return NextResponse.redirect(new URL('/', req.url)) } + return } - if (isMemberRoute(req)) - auth().protect((has) => { - return ( - // TODO check for membership identifier in url and check group - has({ permission: 'org:sys_memberships' }) || has({ permission: 'org:sys_domains_manage' }) - ) - }) + // Check if user is a member of SafeInsights org with si_member role + const isSiMember = organizations?.some( + org => org.id === SAFEINSIGHTS_ORG_ID && org.membership?.role === 'si_member' + ) + + // Member route access control + if (isMemberRoute(req)) { + if (!isSiMember) { + return new NextResponse(null, { status: 403 }) + } + return + } - if (isResearcherRoute(req)) - auth().protect((has) => { - return ( - // TODO setup groups and perms, check them here - has({ permission: 'org:researcher' }) - ) - }) + // Researcher route access control + if (isResearcherRoute(req)) { + if (isSiMember) { + return new NextResponse(null, { status: 403 }) + } + return + } }) export const config = { From df1f3249c962a24d206ae7937151327f6e7d73ef Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:23:33 +0200 Subject: [PATCH 02/23] get organisations --- src/app/page.tsx | 20 ++++++++++++++++++-- src/middleware.ts | 32 +++++++++++++++++++------------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 7e62dd8..20b71fb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,10 +1,26 @@ -import { SignedIn, SignedOut } from '@clerk/nextjs' +'use client' + +import { SignedIn, SignedOut, useOrganizationList, useUser } from '@clerk/nextjs' import { SignIn } from '@/components/signin' import { footerStyles, mainStyles, pageStyles } from './page.css' import Link from 'next/link' import { Button, Title, Paper, Flex } from '@mantine/core' -export default async function Home() { +export default function Home() { + const { user } = useUser() + + const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' + const isSiMember = user?.organizationMemberships?.some( + membership => membership.organization.id === SAFEINSIGHTS_ORG_ID + ) + + const isResearcher = user?.organizationMemberships?.some( + membership => membership.role === 'researcher' + ) + + console.log('Current user:', user) + console.log(`Current User is member of org SafeInsights: ${isSiMember ? 'yes' : 'no'}`) + console.log(`Current User is a Researcher: ${isResearcher ? 'yes' : 'no'}`) return (
diff --git a/src/middleware.ts b/src/middleware.ts index 37d68ee..28e927b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -17,26 +17,32 @@ export default clerkMiddleware(async (auth, req) => { return } - // Check if user is a member of SafeInsights org with si_member role - const isSiMember = organizations?.some( - org => org.id === SAFEINSIGHTS_ORG_ID && org.membership?.role === 'si_member' - ) + // For researcher routes, ensure they're not using SafeInsights org + if (isResearcherRoute(req)) { + return NextResponse.next() + } - // Member route access control + // For member routes, require SafeInsights membership if (isMemberRoute(req)) { + if (!userId) { + return NextResponse.redirect(new URL('/sign-in', req.url)) + } + + const isSiMember = organizations?.some( + org => org.id === SAFEINSIGHTS_ORG_ID && org.membership?.role === 'si_member' + ) + + // Debug logging + console.log('Organizations:', organizations) + console.log('Is SafeInsights member:', isSiMember) + console.log('User ID:', userId) + if (!isSiMember) { return new NextResponse(null, { status: 403 }) } - return } - // Researcher route access control - if (isResearcherRoute(req)) { - if (isSiMember) { - return new NextResponse(null, { status: 403 }) - } - return - } + return NextResponse.next() }) export const config = { From 2e0a8697b3b9ad2a6b2d47053e3f8e09a47e8ee0 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:49:31 +0200 Subject: [PATCH 03/23] close match to si_member verification --- src/app/page.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 20b71fb..e7b4277 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -14,13 +14,9 @@ export default function Home() { membership => membership.organization.id === SAFEINSIGHTS_ORG_ID ) - const isResearcher = user?.organizationMemberships?.some( - membership => membership.role === 'researcher' - ) - console.log('Current user:', user) console.log(`Current User is member of org SafeInsights: ${isSiMember ? 'yes' : 'no'}`) - console.log(`Current User is a Researcher: ${isResearcher ? 'yes' : 'no'}`) + console.log(`Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) return (
From 9168d06d1624f5bc447a901b0d66247e3ea0ce64 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:56:39 +0200 Subject: [PATCH 04/23] getting closer of finding membership in role --- src/app/page.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index e7b4277..239b20d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,12 +10,16 @@ export default function Home() { const { user } = useUser() const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' - const isSiMember = user?.organizationMemberships?.some( + const isOrgMember = user?.organizationMemberships?.some( membership => membership.organization.id === SAFEINSIGHTS_ORG_ID ) + const isSiMember = user?.organizationMemberships?.some( + membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'si_member' + ) + console.log('Current user:', user) - console.log(`Current User is member of org SafeInsights: ${isSiMember ? 'yes' : 'no'}`) + console.log(`Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) console.log(`Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) return (
From f588c7e49de7202a65d2e3644969fdb7c7ce4781 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:59:50 +0200 Subject: [PATCH 05/23] checking role works finally --- src/app/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 239b20d..f792df9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -15,7 +15,7 @@ export default function Home() { ) const isSiMember = user?.organizationMemberships?.some( - membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'si_member' + membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:si_member' ) console.log('Current user:', user) From d500ad50ec2c22ff97843ca11dfcdbb683a82a5a Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:15:07 +0200 Subject: [PATCH 06/23] test out middleware approach --- src/app/page.tsx | 10 +++++-- src/middleware.ts | 75 ++++++++++++++++++++++++++--------------------- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index f792df9..2bbff69 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { SignedIn, SignedOut, useOrganizationList, useUser } from '@clerk/nextjs' +import { SignedIn, SignedOut, useUser } from '@clerk/nextjs' import { SignIn } from '@/components/signin' import { footerStyles, mainStyles, pageStyles } from './page.css' import Link from 'next/link' @@ -10,17 +10,21 @@ export default function Home() { const { user } = useUser() const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' + const isOrgMember = user?.organizationMemberships?.some( membership => membership.organization.id === SAFEINSIGHTS_ORG_ID ) - const isSiMember = user?.organizationMemberships?.some( membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:si_member' ) - + const isAdmin = user?.organizationMemberships?.some( + membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:admin' + ) + console.log('Current user:', user) console.log(`Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) console.log(`Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) + console.log(`Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) return (
diff --git a/src/middleware.ts b/src/middleware.ts index 28e927b..dba29f7 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,48 +1,55 @@ -import { clerkMiddleware, createRouteMatcher, auth } from '@clerk/nextjs/server' +'use server' +import 'server-only' +import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { NextResponse } from 'next/server' +const isMemberRoute = createRouteMatcher(['/fix-me/member(.*)']) +const isResearcherRoute = createRouteMatcher(['/fix-me/researcher(.*)']) -const isMemberRoute = createRouteMatcher(['/member/(.*)']) -const isResearcherRoute = createRouteMatcher(['/researcher/(.*)']) +// Clerk middleware reference +// https://clerk.com/docs/references/nextjs/clerk-middleware -const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' +export default clerkMiddleware((auth, req) => { + const { userId, user } = auth() -export default clerkMiddleware(async (auth, req) => { - const { userId, organizations } = auth - - // Redirect logged-in users away from auth pages + const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' + + const isOrgMember = user?.organizationMemberships?.some( + membership => membership.organization.id === SAFEINSIGHTS_ORG_ID + ) + const isSiMember = user?.organizationMemberships?.some( + membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:si_member' + ) + const isAdmin = user?.organizationMemberships?.some( + membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:admin' + ) + + console.log('[Middleware] Current user:', user) + console.log(`[Middleware] Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) + + // Do not allow and redirect certain paths when logged in (e.g. password resets, signup) if (req.nextUrl.pathname.startsWith('/reset-password') || req.nextUrl.pathname.startsWith('/signup')) { if (userId) { return NextResponse.redirect(new URL('/', req.url)) } - return - } - - // For researcher routes, ensure they're not using SafeInsights org - if (isResearcherRoute(req)) { - return NextResponse.next() } - // For member routes, require SafeInsights membership - if (isMemberRoute(req)) { - if (!userId) { - return NextResponse.redirect(new URL('/sign-in', req.url)) - } - - const isSiMember = organizations?.some( - org => org.id === SAFEINSIGHTS_ORG_ID && org.membership?.role === 'si_member' - ) - - // Debug logging - console.log('Organizations:', organizations) - console.log('Is SafeInsights member:', isSiMember) - console.log('User ID:', userId) - - if (!isSiMember) { - return new NextResponse(null, { status: 403 }) - } - } + if (isMemberRoute(req)) + auth().protect((has) => { + return ( + // TODO check for membership identifier in url and check group + has({ permission: 'org:sys_memberships' }) || has({ permission: 'org:sys_domains_manage' }) + ) + }) - return NextResponse.next() + if (isResearcherRoute(req)) + auth().protect((has) => { + return ( + // TODO setup groups and perms, check them here + has({ permission: 'org:researcher' }) + ) + }) }) export const config = { From 6c3490383dd3ee48663d8796c8f0277ad7058937 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:27:12 +0200 Subject: [PATCH 07/23] middleware changes tests --- src/middleware.ts | 86 +++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index dba29f7..a0aebd1 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,62 +1,62 @@ -'use server' -import 'server-only' import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' + const isMemberRoute = createRouteMatcher(['/fix-me/member(.*)']) const isResearcherRoute = createRouteMatcher(['/fix-me/researcher(.*)']) +const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware -export default clerkMiddleware((auth, req) => { - const { userId, user } = auth() - - const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' - - const isOrgMember = user?.organizationMemberships?.some( - membership => membership.organization.id === SAFEINSIGHTS_ORG_ID - ) - const isSiMember = user?.organizationMemberships?.some( - membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:si_member' - ) - const isAdmin = user?.organizationMemberships?.some( - membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:admin' - ) - - console.log('[Middleware] Current user:', user) - console.log(`[Middleware] Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) - console.log(`[Middleware] Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) - console.log(`[Middleware] Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) - - // Do not allow and redirect certain paths when logged in (e.g. password resets, signup) - if (req.nextUrl.pathname.startsWith('/reset-password') || req.nextUrl.pathname.startsWith('/signup')) { - if (userId) { - return NextResponse.redirect(new URL('/', req.url)) +export default clerkMiddleware(async (auth, req: NextRequest) => { + try { + const { userId, orgRole, orgId } = await auth() + + // Check organization memberships + const isOrgMember = (orgId === SAFEINSIGHTS_ORG_ID) + const isSiMember = (orgRole === 'org:si_member') + const isAdmin = (orgRole === 'org:admin') + + // console.log('Current user:', user) + console.log(`[Middleware] Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) + + // Handle authentication redirects + if (req.nextUrl.pathname.startsWith('/reset-password') || + req.nextUrl.pathname.startsWith('/signup')) { + if (userId) { + return NextResponse.redirect(new URL('/', req.url)) + } } + + // // Handle route protection + // if (isMemberRoute(req)) { + // if (!userId || !isOrgMember) { + // return NextResponse.redirect(new URL('/sign-in', req.url)) + // } + // } + + // if (isResearcherRoute(req)) { + // if (!userId || !isAdmin) { + // return NextResponse.redirect(new URL('/sign-in', req.url)) + // } + // } + + } catch (error) { + console.error('Middleware error:', error) } - if (isMemberRoute(req)) - auth().protect((has) => { - return ( - // TODO check for membership identifier in url and check group - has({ permission: 'org:sys_memberships' }) || has({ permission: 'org:sys_domains_manage' }) - ) - }) - - if (isResearcherRoute(req)) - auth().protect((has) => { - return ( - // TODO setup groups and perms, check them here - has({ permission: 'org:researcher' }) - ) - }) + return NextResponse.next() }) export const config = { matcher: [ // Skip Next.js internals and all static files, unless found in search params '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', - // Always run for routes above + // Always run for routes below '/(member|researcher)(.*)', + '/', + '/(reset-password|signup)' ], } From 1ffd197fea3c76377a0f9943e0b59ec1e1b5f61d Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:36:31 +0200 Subject: [PATCH 08/23] enforce organisation selection on login --- src/app/org-selection/page.tsx | 16 ++++++++++++++++ src/app/page.tsx | 33 +++++++++++++++++---------------- src/middleware.ts | 19 +++++++++++++------ 3 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 src/app/org-selection/page.tsx diff --git a/src/app/org-selection/page.tsx b/src/app/org-selection/page.tsx new file mode 100644 index 0000000..8d5482a --- /dev/null +++ b/src/app/org-selection/page.tsx @@ -0,0 +1,16 @@ +'use client' + +import { OrganizationList } from '@clerk/nextjs' +import { Center } from '@mantine/core' + +export default function OrgSelection() { + return ( +
+ +
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 2bbff69..0ef6b72 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,30 +1,25 @@ 'use client' -import { SignedIn, SignedOut, useUser } from '@clerk/nextjs' +import { SignedIn, SignedOut, useUser, OrganizationSwitcher } from '@clerk/nextjs' import { SignIn } from '@/components/signin' +import { useOrganization } from '@clerk/nextjs' import { footerStyles, mainStyles, pageStyles } from './page.css' import Link from 'next/link' import { Button, Title, Paper, Flex } from '@mantine/core' export default function Home() { const { user } = useUser() - + const { organization } = useOrganization() + const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' + + const isOrgMember = organization?.id === SAFEINSIGHTS_ORG_ID + const isSiMember = isOrgMember && organization?.membership?.role === 'org:si_member' + const isAdmin = isOrgMember && organization?.membership?.role === 'org:admin' - const isOrgMember = user?.organizationMemberships?.some( - membership => membership.organization.id === SAFEINSIGHTS_ORG_ID - ) - const isSiMember = user?.organizationMemberships?.some( - membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:si_member' - ) - const isAdmin = user?.organizationMemberships?.some( - membership => membership.organization.id === SAFEINSIGHTS_ORG_ID && membership.role === 'org:admin' - ) - - console.log('Current user:', user) - console.log(`Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) - console.log(`Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) - console.log(`Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) + console.log('Current organization:', organization) + console.log(`Active in SafeInsights org: ${isOrgMember ? 'yes' : 'no'}`) + console.log(`Current role: ${organization?.membership?.role}`) return (
@@ -34,6 +29,12 @@ export default function Home() {
+ Welcome to the SafeInsights management app. You likely want to visit the OpenStax study proposal page. diff --git a/src/middleware.ts b/src/middleware.ts index a0aebd1..43f05fe 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -10,14 +10,21 @@ const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' export default clerkMiddleware(async (auth, req: NextRequest) => { try { - const { userId, orgRole, orgId } = await auth() + const { userId, orgId, orgRole, sessionClaims } = await auth() - // Check organization memberships - const isOrgMember = (orgId === SAFEINSIGHTS_ORG_ID) - const isSiMember = (orgRole === 'org:si_member') - const isAdmin = (orgRole === 'org:admin') + // Require organization selection and prevent personal account usage + if (userId && (!orgId || sessionClaims?.org_personal)) { + if (!req.nextUrl.pathname.startsWith('/org-selection')) { + return NextResponse.redirect(new URL('/org-selection', req.url)) + } + } + + const isOrgMember = orgId === SAFEINSIGHTS_ORG_ID + const isSiMember = isOrgMember && orgRole === 'org:si_member' + const isAdmin = isOrgMember && orgRole === 'org:admin' - // console.log('Current user:', user) + console.log('[Middleware] Active Organization:', orgId) + console.log('[Middleware] Current Role:', orgRole) console.log(`[Middleware] Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) console.log(`[Middleware] Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) console.log(`[Middleware] Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) From df1407991cfb14900be44487d99e067bcd0d7767 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:55:41 +0200 Subject: [PATCH 09/23] change org to Openstax --- src/app/page.tsx | 6 +++--- src/middleware.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0ef6b72..88deaca 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,14 +11,14 @@ export default function Home() { const { user } = useUser() const { organization } = useOrganization() - const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' + const OPENSTAX_ORG_ID = 'org_2ohzjhfpKp4QqubW86FfXzzDm2I' - const isOrgMember = organization?.id === SAFEINSIGHTS_ORG_ID + const isOrgMember = organization?.id === OPENSTAX_ORG_ID const isSiMember = isOrgMember && organization?.membership?.role === 'org:si_member' const isAdmin = isOrgMember && organization?.membership?.role === 'org:admin' console.log('Current organization:', organization) - console.log(`Active in SafeInsights org: ${isOrgMember ? 'yes' : 'no'}`) + console.log(`Active in openstax org: ${isOrgMember ? 'yes' : 'no'}`) console.log(`Current role: ${organization?.membership?.role}`) return (
diff --git a/src/middleware.ts b/src/middleware.ts index 43f05fe..2de2167 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from 'next/server' const isMemberRoute = createRouteMatcher(['/fix-me/member(.*)']) const isResearcherRoute = createRouteMatcher(['/fix-me/researcher(.*)']) -const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' +const OPENSTAX_ORG_ID = 'org_2ohzjhfpKp4QqubW86FfXzzDm2I' // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware @@ -19,13 +19,13 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { } } - const isOrgMember = orgId === SAFEINSIGHTS_ORG_ID + const isOrgMember = orgId === OPENSTAX_ORG_ID const isSiMember = isOrgMember && orgRole === 'org:si_member' const isAdmin = isOrgMember && orgRole === 'org:admin' console.log('[Middleware] Active Organization:', orgId) console.log('[Middleware] Current Role:', orgRole) - console.log(`[Middleware] Current User is member of org SafeInsights: ${isOrgMember ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is member of org openstax: ${isOrgMember ? 'yes' : 'no'}`) console.log(`[Middleware] Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) console.log(`[Middleware] Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) From c4225f1a7ad8cff56d21972a58d720ecd2b848b9 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:44:08 +0200 Subject: [PATCH 10/23] protect routes and automatically redirect after login --- src/app/page.tsx | 2 +- src/middleware.ts | 38 ++++++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 88deaca..0d6877f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,7 +19,7 @@ export default function Home() { console.log('Current organization:', organization) console.log(`Active in openstax org: ${isOrgMember ? 'yes' : 'no'}`) - console.log(`Current role: ${organization?.membership?.role}`) + console.log(`Is si_member: ${isSiMember ? 'yes' : 'no'}`) return (
diff --git a/src/middleware.ts b/src/middleware.ts index 2de2167..3747ed6 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,8 +1,8 @@ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' -const isMemberRoute = createRouteMatcher(['/fix-me/member(.*)']) -const isResearcherRoute = createRouteMatcher(['/fix-me/researcher(.*)']) +const isMemberRoute = createRouteMatcher(['/member(.*)']) +const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) const OPENSTAX_ORG_ID = 'org_2ohzjhfpKp4QqubW86FfXzzDm2I' // Clerk middleware reference @@ -18,7 +18,7 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { return NextResponse.redirect(new URL('/org-selection', req.url)) } } - + const isOrgMember = orgId === OPENSTAX_ORG_ID const isSiMember = isOrgMember && orgRole === 'org:si_member' const isAdmin = isOrgMember && orgRole === 'org:admin' @@ -37,18 +37,28 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { } } - // // Handle route protection - // if (isMemberRoute(req)) { - // if (!userId || !isOrgMember) { - // return NextResponse.redirect(new URL('/sign-in', req.url)) - // } - // } + // Handle post-login redirects for members and researchers + if (userId && isOrgMember && req.nextUrl.pathname === '/') { + if (isSiMember) { + return NextResponse.redirect(new URL('/member/openstax/studies/review', req.url)) + } else { + return NextResponse.redirect(new URL('/researcher/study/request/openstax', req.url)) + } + } + + // Handle member route protection + if (isMemberRoute(req)) { + if (!isSiMember && !isAdmin) { + return new NextResponse(null, { status: 403 }) + } + } - // if (isResearcherRoute(req)) { - // if (!userId || !isAdmin) { - // return NextResponse.redirect(new URL('/sign-in', req.url)) - // } - // } + // Handle researcher route protection + if (isResearcherRoute(req)) { + if (isSiMember) { + return new NextResponse(null, { status: 403 }) + } + } } catch (error) { console.error('Middleware error:', error) From dd5b6e022cc48b2728f9165d84fec59f3c75b911 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:34:08 +0200 Subject: [PATCH 11/23] fix up logic --- src/middleware.ts | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 3747ed6..95d0ef8 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -4,6 +4,7 @@ import { NextRequest, NextResponse } from 'next/server' const isMemberRoute = createRouteMatcher(['/member(.*)']) const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) const OPENSTAX_ORG_ID = 'org_2ohzjhfpKp4QqubW86FfXzzDm2I' +const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware @@ -19,15 +20,17 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { } } - const isOrgMember = orgId === OPENSTAX_ORG_ID - const isSiMember = isOrgMember && orgRole === 'org:si_member' - const isAdmin = isOrgMember && orgRole === 'org:admin' + // Check if user belongs to SafeInsights organization (admin - highest priority) + const isAdmin = orgId === SAFEINSIGHTS_ORG_ID + // Check if user belongs to OpenStax organization (if not admin) + const isOrgMember = !isAdmin && orgId === OPENSTAX_ORG_ID + // Check if user is a SafeInsights member (any OpenStax org member, if not admin) + const isSiMember = isOrgMember console.log('[Middleware] Active Organization:', orgId) console.log('[Middleware] Current Role:', orgRole) - console.log(`[Middleware] Current User is member of org openstax: ${isOrgMember ? 'yes' : 'no'}`) - console.log(`[Middleware] Current User is a SafeInsights member (si_member): ${isSiMember ? 'yes' : 'no'}`) - console.log(`[Middleware] Current User is a SafeInsights admin (admin): ${isAdmin ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is admin: ${isAdmin ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is si_member: ${isSiMember ? 'yes' : 'no'}`) // Handle authentication redirects if (req.nextUrl.pathname.startsWith('/reset-password') || @@ -37,25 +40,33 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { } } + // TODO: Activate it for future usage if needed // Handle post-login redirects for members and researchers - if (userId && isOrgMember && req.nextUrl.pathname === '/') { - if (isSiMember) { - return NextResponse.redirect(new URL('/member/openstax/studies/review', req.url)) - } else { - return NextResponse.redirect(new URL('/researcher/study/request/openstax', req.url)) - } - } + // if (userId && isOrgMember && req.nextUrl.pathname === '/') { + // if (isMember) { + // return NextResponse.redirect(new URL('/member/openstax/studies/review', req.url)) + // } else { + // return NextResponse.redirect(new URL('/researcher/study/request/openstax', req.url)) + // } + // } + + // Define researcher status (users not admin and not in OpenStax) + const isResearcher = !isAdmin && !isOrgMember // Handle member route protection if (isMemberRoute(req)) { + // Only SI members and admins can access member routes if (!isSiMember && !isAdmin) { + console.log('[Middleware] Access denied: Member route requires SI member or admin access') return new NextResponse(null, { status: 403 }) } } // Handle researcher route protection if (isResearcherRoute(req)) { - if (isSiMember) { + // Only researchers and admins can access researcher routes + if (!isResearcher && !isAdmin) { + console.log('[Middleware] Access denied: Researcher route requires researcher or admin access') return new NextResponse(null, { status: 403 }) } } From a512aff200fc4a5062207c77add6570642877d24 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:50:24 +0200 Subject: [PATCH 12/23] refactor code --- src/middleware.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 95d0ef8..f1252c8 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -13,24 +13,28 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { try { const { userId, orgId, orgRole, sessionClaims } = await auth() + // TODO: Probably remove // Require organization selection and prevent personal account usage - if (userId && (!orgId || sessionClaims?.org_personal)) { - if (!req.nextUrl.pathname.startsWith('/org-selection')) { - return NextResponse.redirect(new URL('/org-selection', req.url)) - } - } + // if (userId && (!orgId || sessionClaims?.org_personal)) { + // if (!req.nextUrl.pathname.startsWith('/org-selection')) { + // return NextResponse.redirect(new URL('/org-selection', req.url)) + // } + // } // Check if user belongs to SafeInsights organization (admin - highest priority) const isAdmin = orgId === SAFEINSIGHTS_ORG_ID // Check if user belongs to OpenStax organization (if not admin) const isOrgMember = !isAdmin && orgId === OPENSTAX_ORG_ID // Check if user is a SafeInsights member (any OpenStax org member, if not admin) - const isSiMember = isOrgMember + const isMember = isOrgMember + // Define researcher status (users not admin and not in OpenStax) + const isResearcher = !isAdmin && !isOrgMember console.log('[Middleware] Active Organization:', orgId) console.log('[Middleware] Current Role:', orgRole) console.log(`[Middleware] Current User is admin: ${isAdmin ? 'yes' : 'no'}`) - console.log(`[Middleware] Current User is si_member: ${isSiMember ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is si_member: ${isMember ? 'yes' : 'no'}`) + console.log(`[Middleware] Current User is si_researcher: ${isResearcher ? 'yes' : 'no'}`) // Handle authentication redirects if (req.nextUrl.pathname.startsWith('/reset-password') || @@ -50,13 +54,10 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { // } // } - // Define researcher status (users not admin and not in OpenStax) - const isResearcher = !isAdmin && !isOrgMember - // Handle member route protection if (isMemberRoute(req)) { // Only SI members and admins can access member routes - if (!isSiMember && !isAdmin) { + if (!isMember && !isAdmin) { console.log('[Middleware] Access denied: Member route requires SI member or admin access') return new NextResponse(null, { status: 403 }) } From f7dbd877ed95ec9ca304e9b653b695a2ae4f1b76 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:52:05 +0200 Subject: [PATCH 13/23] remove org selector for now --- src/app/org-selection/page.tsx | 16 ---------------- src/app/page.tsx | 8 +------- src/middleware.ts | 7 ------- 3 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 src/app/org-selection/page.tsx diff --git a/src/app/org-selection/page.tsx b/src/app/org-selection/page.tsx deleted file mode 100644 index 8d5482a..0000000 --- a/src/app/org-selection/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client' - -import { OrganizationList } from '@clerk/nextjs' -import { Center } from '@mantine/core' - -export default function OrgSelection() { - return ( -
- -
- ) -} diff --git a/src/app/page.tsx b/src/app/page.tsx index 0d6877f..2e77ba0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { SignedIn, SignedOut, useUser, OrganizationSwitcher } from '@clerk/nextjs' +import { SignedIn, SignedOut, useUser } from '@clerk/nextjs' import { SignIn } from '@/components/signin' import { useOrganization } from '@clerk/nextjs' import { footerStyles, mainStyles, pageStyles } from './page.css' @@ -29,12 +29,6 @@ export default function Home() {
- Welcome to the SafeInsights management app. You likely want to visit the OpenStax study proposal page. diff --git a/src/middleware.ts b/src/middleware.ts index f1252c8..eed89a3 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -13,13 +13,6 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { try { const { userId, orgId, orgRole, sessionClaims } = await auth() - // TODO: Probably remove - // Require organization selection and prevent personal account usage - // if (userId && (!orgId || sessionClaims?.org_personal)) { - // if (!req.nextUrl.pathname.startsWith('/org-selection')) { - // return NextResponse.redirect(new URL('/org-selection', req.url)) - // } - // } // Check if user belongs to SafeInsights organization (admin - highest priority) const isAdmin = orgId === SAFEINSIGHTS_ORG_ID From 9b3b76dbf47390fb5bb2555a192dd13b0a30337f Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:28:36 +0200 Subject: [PATCH 14/23] revert changes in main page --- src/app/page.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index e58a49d..92f2f77 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,25 +1,10 @@ -'use client' - -import { SignedIn, SignedOut, useUser } from '@clerk/nextjs' +import { SignedIn, SignedOut } from '@clerk/nextjs' import { SignIn } from '@/components/signin' -import { useOrganization } from '@clerk/nextjs' import { footerStyles, mainStyles, pageStyles } from './page.css' import { Title, Flex } from '@mantine/core' import { UserNav } from './user-nav' -export default function Home() { - const { user } = useUser() - const { organization } = useOrganization() - - const OPENSTAX_ORG_ID = 'org_2ohzjhfpKp4QqubW86FfXzzDm2I' - - const isOrgMember = organization?.id === OPENSTAX_ORG_ID - const isSiMember = isOrgMember && organization?.membership?.role === 'org:si_member' - const isAdmin = isOrgMember && organization?.membership?.role === 'org:admin' - - console.log('Current organization:', organization) - console.log(`Active in openstax org: ${isOrgMember ? 'yes' : 'no'}`) - console.log(`Is si_member: ${isSiMember ? 'yes' : 'no'}`) +export default async function Home() { return (
From 92a2221223033bebab1c2623f9d08b3327781a2f Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:36:21 +0200 Subject: [PATCH 15/23] use a simple logger for debug info of middleware --- src/lib/logger.ts | 24 ++++++++++++++++++++++++ src/middleware.ts | 19 +++++++++++-------- 2 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 src/lib/logger.ts diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..87d5153 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,24 @@ +type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +const isDevelopment = process.env.NODE_ENV === 'development' + +const logger = { + debug: (...args: unknown[]) => { + if (isDevelopment) { + console.warn('[DEBUG]', ...args) + } + }, + info: (...args: unknown[]) => { + if (isDevelopment) { + console.warn('[INFO]', ...args) + } + }, + warn: (...args: unknown[]) => { + console.warn('[WARN]', ...args) + }, + error: (...args: unknown[]) => { + console.error('[ERROR]', ...args) + }, +} + +export default logger diff --git a/src/middleware.ts b/src/middleware.ts index e397b09..ca28a91 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,5 +1,6 @@ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' +import logger from '@/lib/logger' const isMemberRoute = createRouteMatcher(['/member(.*)']) const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) @@ -23,11 +24,13 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { // Define researcher status (users not admin and not in OpenStax) const isResearcher = !isAdmin && !isOrgMember - console.log('[Middleware] Active Organization:', orgId) - console.log('[Middleware] Current Role:', orgRole) - console.log(`[Middleware] Current User is admin: ${isAdmin ? 'yes' : 'no'}`) - console.log(`[Middleware] Current User is si_member: ${isMember ? 'yes' : 'no'}`) - console.log(`[Middleware] Current User is si_researcher: ${isResearcher ? 'yes' : 'no'}`) + logger.info('Middleware:', { + organization: orgId, + role: orgRole, + isAdmin, + isMember, + isResearcher, + }) // Handle authentication redirects if (req.nextUrl.pathname.startsWith('/reset-password') || @@ -51,7 +54,7 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { if (isMemberRoute(req)) { // Only SI members and admins can access member routes if (!isMember && !isAdmin) { - console.log('[Middleware] Access denied: Member route requires SI member or admin access') + logger.warn('Access denied: Member route requires SI member or admin access') return new NextResponse(null, { status: 403 }) } } @@ -60,13 +63,13 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { if (isResearcherRoute(req)) { // Only researchers and admins can access researcher routes if (!isResearcher && !isAdmin) { - console.log('[Middleware] Access denied: Researcher route requires researcher or admin access') + logger.warn('Access denied: Researcher route requires researcher or admin access') return new NextResponse(null, { status: 403 }) } } } catch (error) { - console.error('Middleware error:', error) + logger.error('Middleware error:', error) } return NextResponse.next() From 9294290a447fc8157dcec1ebcf2d9e5c1ba653c0 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Wed, 13 Nov 2024 23:40:56 +0200 Subject: [PATCH 16/23] add Clerk auth() response example as TypeDoc comment --- src/middleware.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/middleware.ts b/src/middleware.ts index ca28a91..66748d0 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -2,6 +2,34 @@ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' import logger from '@/lib/logger' +/** + * Example Clerk auth() response structure: + * ```typescript + * { + * sessionClaims: { + * azp: "http://localhost:4000", + * exp: 1730995945, + * iat: 1730995885, + * iss: "https://example.clerk.accounts.dev", + * nbf: 1730995875, + * org_id: "org_xxxxxxxxxxxxxxxxxxxx", + * org_permissions: [], + * org_role: "org:admin", + * org_slug: "example-org", + * sid: "sess_xxxxxxxxxxxxxxxxxxxx", + * sub: "user_xxxxxxxxxxxxxxxxxxxx" + * }, + * sessionId: "sess_xxxxxxxxxxxxxxxxxxxx", + * userId: "user_xxxxxxxxxxxxxxxxxxxx", + * orgId: "org_xxxxxxxxxxxxxxxxxxxx", + * orgRole: "org:admin", + * orgSlug: "example-org", + * orgPermissions: [], + * __experimental_factorVerificationAge: null + * } + * ``` + */ + const isMemberRoute = createRouteMatcher(['/member(.*)']) const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) const OPENSTAX_ORG_ID = 'org_2ohzjhfpKp4QqubW86FfXzzDm2I' @@ -12,7 +40,7 @@ const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' export default clerkMiddleware(async (auth, req: NextRequest) => { try { - const { userId, orgId, orgRole, sessionClaims } = await auth() + const { userId, orgId, orgRole } = await auth() // Check if user belongs to SafeInsights organization (admin - highest priority) From ec2716af0778e091fb8868950061c89050f9551f Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:01:52 +0200 Subject: [PATCH 17/23] make everything nice --- src/middleware.ts | 74 +++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 66748d0..def1ad9 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,4 +1,4 @@ -import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' +import { clerkMiddleware, createRouteMatcher, AuthObject } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' import logger from '@/lib/logger' @@ -38,26 +38,37 @@ const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware -export default clerkMiddleware(async (auth, req: NextRequest) => { +export default clerkMiddleware(async (auth: AuthObject, req: NextRequest) => { try { const { userId, orgId, orgRole } = await auth() + if (!userId) { + // Block unauthenticated access to protected routes + if (isMemberRoute(req) || isResearcherRoute(req)) { + logger.warn('Access denied: Authentication required') + return new NextResponse(null, { status: 403 }) + } + // For non-protected routes, let Clerk handle the redirect + return NextResponse.next() + } - // Check if user belongs to SafeInsights organization (admin - highest priority) - const isAdmin = orgId === SAFEINSIGHTS_ORG_ID - // Check if user belongs to OpenStax organization (if not admin) - const isOrgMember = !isAdmin && orgId === OPENSTAX_ORG_ID - // Check if user is a SafeInsights member (any OpenStax org member, if not admin) - const isMember = isOrgMember - // Define researcher status (users not admin and not in OpenStax) - const isResearcher = !isAdmin && !isOrgMember + + // Define user roles + const userRoles = { + isAdmin: orgId === SAFEINSIGHTS_ORG_ID, + isOpenStaxMember: orgId === OPENSTAX_ORG_ID, + get isSafeInsightsMember() { + return this.isOpenStaxMember && !this.isAdmin + }, + get isResearcher() { + return !this.isAdmin && !this.isOpenStaxMember + } + } logger.info('Middleware:', { organization: orgId, role: orgRole, - isAdmin, - isMember, - isResearcher, + ...userRoles }) // Handle authentication redirects @@ -68,32 +79,21 @@ export default clerkMiddleware(async (auth, req: NextRequest) => { } } - // TODO: Activate it for future usage if needed - // Handle post-login redirects for members and researchers - // if (userId && isOrgMember && req.nextUrl.pathname === '/') { - // if (isMember) { - // return NextResponse.redirect(new URL('/member/openstax/studies/review', req.url)) - // } else { - // return NextResponse.redirect(new URL('/researcher/study/request/openstax', req.url)) - // } - // } - - // Handle member route protection - if (isMemberRoute(req)) { - // Only SI members and admins can access member routes - if (!isMember && !isAdmin) { - logger.warn('Access denied: Member route requires SI member or admin access') - return new NextResponse(null, { status: 403 }) - } + + // Route protection + const routeProtection = { + member: isMemberRoute(req) && !userRoles.isSafeInsightsMember && !userRoles.isAdmin, + researcher: isResearcherRoute(req) && !userRoles.isResearcher && !userRoles.isAdmin } - // Handle researcher route protection - if (isResearcherRoute(req)) { - // Only researchers and admins can access researcher routes - if (!isResearcher && !isAdmin) { - logger.warn('Access denied: Researcher route requires researcher or admin access') - return new NextResponse(null, { status: 403 }) - } + if (routeProtection.member) { + logger.warn('Access denied: Member route requires SI member or admin access') + return new NextResponse(null, { status: 403 }) + } + + if (routeProtection.researcher) { + logger.warn('Access denied: Researcher route requires researcher or admin access') + return new NextResponse(null, { status: 403 }) } } catch (error) { From df0095696601fbe397901735f1869c25c8c5f494 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:16:48 +0200 Subject: [PATCH 18/23] fix type definitons --- src/lib/logger.ts | 2 -- src/middleware.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 87d5153..f8098a8 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,5 +1,3 @@ -type LogLevel = 'debug' | 'info' | 'warn' | 'error' - const isDevelopment = process.env.NODE_ENV === 'development' const logger = { diff --git a/src/middleware.ts b/src/middleware.ts index def1ad9..bcdbdad 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,4 +1,4 @@ -import { clerkMiddleware, createRouteMatcher, AuthObject } from '@clerk/nextjs/server' +import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' import logger from '@/lib/logger' @@ -38,7 +38,7 @@ const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware -export default clerkMiddleware(async (auth: AuthObject, req: NextRequest) => { +export default clerkMiddleware(async (auth: any, req: NextRequest) => { try { const { userId, orgId, orgRole } = await auth() From 526b80d5f7067138bc547808b85aa94dae30c1fb Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:33:30 +0200 Subject: [PATCH 19/23] prettier --- src/middleware.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index bcdbdad..7fc5957 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -52,7 +52,6 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { return NextResponse.next() } - // Define user roles const userRoles = { isAdmin: orgId === SAFEINSIGHTS_ORG_ID, @@ -62,28 +61,26 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { }, get isResearcher() { return !this.isAdmin && !this.isOpenStaxMember - } + }, } logger.info('Middleware:', { organization: orgId, role: orgRole, - ...userRoles + ...userRoles, }) // Handle authentication redirects - if (req.nextUrl.pathname.startsWith('/reset-password') || - req.nextUrl.pathname.startsWith('/signup')) { + if (req.nextUrl.pathname.startsWith('/reset-password') || req.nextUrl.pathname.startsWith('/signup')) { if (userId) { return NextResponse.redirect(new URL('/', req.url)) } } - // Route protection const routeProtection = { member: isMemberRoute(req) && !userRoles.isSafeInsightsMember && !userRoles.isAdmin, - researcher: isResearcherRoute(req) && !userRoles.isResearcher && !userRoles.isAdmin + researcher: isResearcherRoute(req) && !userRoles.isResearcher && !userRoles.isAdmin, } if (routeProtection.member) { @@ -95,7 +92,6 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { logger.warn('Access denied: Researcher route requires researcher or admin access') return new NextResponse(null, { status: 403 }) } - } catch (error) { logger.error('Middleware error:', error) } @@ -110,6 +106,6 @@ export const config = { // Always run for routes below '/(dl|member|researcher)(.*)', '/', - '/(reset-password|signup)' + '/(reset-password|signup)', ], } From 838b5bd7dc63a21285b76cd603e4b8ad84eae71a Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:02:36 +0200 Subject: [PATCH 20/23] tidy up naming --- src/middleware.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 7fc5957..80e7a59 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -56,7 +56,7 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { const userRoles = { isAdmin: orgId === SAFEINSIGHTS_ORG_ID, isOpenStaxMember: orgId === OPENSTAX_ORG_ID, - get isSafeInsightsMember() { + get isMember() { return this.isOpenStaxMember && !this.isAdmin }, get isResearcher() { @@ -79,12 +79,12 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { // Route protection const routeProtection = { - member: isMemberRoute(req) && !userRoles.isSafeInsightsMember && !userRoles.isAdmin, + member: isMemberRoute(req) && !userRoles.isMember && !userRoles.isAdmin, researcher: isResearcherRoute(req) && !userRoles.isResearcher && !userRoles.isAdmin, } if (routeProtection.member) { - logger.warn('Access denied: Member route requires SI member or admin access') + logger.warn('Access denied: Member route requires member or admin access') return new NextResponse(null, { status: 403 }) } From 996545a129443135766305d66e0d0953e38a5bd8 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:18:20 +0200 Subject: [PATCH 21/23] use package debug for logging --- package-lock.json | 19 +++++++++++++++++++ package.json | 2 ++ src/lib/logger.ts | 21 ++++++++++----------- src/middleware.ts | 8 +++++++- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 590dd10..10b0a71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "child_process": "^1.0.2", "clsx": "^2.1.1", "dayjs": "^1.11.13", + "debug": "^4.3.7", "highlight.js": "^11.10.0", "jsonwebtoken": "^9.0.2", "kysely": "^0.27.4", @@ -54,6 +55,7 @@ "@playwright/test": "^1.47", "@testing-library/react": "^16.0", "@testing-library/user-event": "^14.5.2", + "@types/debug": "^4.1.12", "@types/pg": "^8.11.10", "@types/react": "18.3", "@typescript-eslint/eslint-plugin": "^8.13.0", @@ -6356,6 +6358,16 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -6383,6 +6395,13 @@ "@types/node": "*" } }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mysql": { "version": "2.15.26", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", diff --git a/package.json b/package.json index c75544d..74c4734 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@playwright/test": "^1.47", "@testing-library/react": "^16.0", "@testing-library/user-event": "^14.5.2", + "@types/debug": "^4.1.12", "@types/pg": "^8.11.10", "@types/react": "18.3", "@typescript-eslint/eslint-plugin": "^8.13.0", @@ -77,6 +78,7 @@ "child_process": "^1.0.2", "clsx": "^2.1.1", "dayjs": "^1.11.13", + "debug": "^4.3.7", "highlight.js": "^11.10.0", "jsonwebtoken": "^9.0.2", "kysely": "^0.27.4", diff --git a/src/lib/logger.ts b/src/lib/logger.ts index f8098a8..1f07b68 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,22 +1,21 @@ -const isDevelopment = process.env.NODE_ENV === 'development' +import debug from 'debug' const logger = { - debug: (...args: unknown[]) => { - if (isDevelopment) { - console.warn('[DEBUG]', ...args) - } - }, - info: (...args: unknown[]) => { - if (isDevelopment) { - console.warn('[INFO]', ...args) - } - }, + debug: debug('app:debug'), + info: debug('app:info'), warn: (...args: unknown[]) => { + debug('app:warn')(...args) console.warn('[WARN]', ...args) }, error: (...args: unknown[]) => { + debug('app:error')(...args) console.error('[ERROR]', ...args) }, } +// Enable debug output in development +if (process.env.NODE_ENV === 'development') { + debug.enable('app:*') +} + export default logger diff --git a/src/middleware.ts b/src/middleware.ts index 80e7a59..a9e676a 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,9 @@ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { NextRequest, NextResponse } from 'next/server' import logger from '@/lib/logger' +import debug from 'debug' + +const middlewareDebug = debug('app:middleware') /** * Example Clerk auth() response structure: @@ -46,6 +49,7 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { // Block unauthenticated access to protected routes if (isMemberRoute(req) || isResearcherRoute(req)) { logger.warn('Access denied: Authentication required') + middlewareDebug('Blocking unauthenticated access to protected route') return new NextResponse(null, { status: 403 }) } // For non-protected routes, let Clerk handle the redirect @@ -64,7 +68,7 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { }, } - logger.info('Middleware:', { + middlewareDebug('Auth check: %o', { organization: orgId, role: orgRole, ...userRoles, @@ -85,11 +89,13 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { if (routeProtection.member) { logger.warn('Access denied: Member route requires member or admin access') + middlewareDebug('Blocking unauthorized member route access: %o', { userId, orgId, userRoles }) return new NextResponse(null, { status: 403 }) } if (routeProtection.researcher) { logger.warn('Access denied: Researcher route requires researcher or admin access') + middlewareDebug('Blocking unauthorized researcher route access: %o', { userId, orgId, userRoles }) return new NextResponse(null, { status: 403 }) } } catch (error) { From 326ce348fdd2eb802af88ff1665436d99fa3887e Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:21:47 +0200 Subject: [PATCH 22/23] replace org IDs with slugs --- src/middleware.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index a9e676a..76f73e8 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -35,15 +35,15 @@ const middlewareDebug = debug('app:middleware') const isMemberRoute = createRouteMatcher(['/member(.*)']) const isResearcherRoute = createRouteMatcher(['/researcher(.*)']) -const OPENSTAX_ORG_ID = 'org_2ohzjhfpKp4QqubW86FfXzzDm2I' -const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD' +const OPENSTAX_ORG_SLUG = 'openstax' +const SAFEINSIGHTS_ORG_SLUG = 'safe-insights' // Clerk middleware reference // https://clerk.com/docs/references/nextjs/clerk-middleware export default clerkMiddleware(async (auth: any, req: NextRequest) => { try { - const { userId, orgId, orgRole } = await auth() + const { userId, orgId, orgRole, orgSlug } = await auth() if (!userId) { // Block unauthenticated access to protected routes @@ -58,8 +58,8 @@ export default clerkMiddleware(async (auth: any, req: NextRequest) => { // Define user roles const userRoles = { - isAdmin: orgId === SAFEINSIGHTS_ORG_ID, - isOpenStaxMember: orgId === OPENSTAX_ORG_ID, + isAdmin: orgSlug === SAFEINSIGHTS_ORG_SLUG, + isOpenStaxMember: orgSlug === OPENSTAX_ORG_SLUG, get isMember() { return this.isOpenStaxMember && !this.isAdmin }, From 2dbc39f699c2da332b9985036a7f0b70b005c3a9 Mon Sep 17 00:00:00 2001 From: therealmarv <1050582+therealmarv@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:33:02 +0200 Subject: [PATCH 23/23] simplify logger --- src/lib/logger.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 1f07b68..4f99709 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -3,14 +3,8 @@ import debug from 'debug' const logger = { debug: debug('app:debug'), info: debug('app:info'), - warn: (...args: unknown[]) => { - debug('app:warn')(...args) - console.warn('[WARN]', ...args) - }, - error: (...args: unknown[]) => { - debug('app:error')(...args) - console.error('[ERROR]', ...args) - }, + warn: debug('app:warn'), + error: debug('app:error'), } // Enable debug output in development @@ -18,4 +12,8 @@ if (process.env.NODE_ENV === 'development') { debug.enable('app:*') } +// Forward warnings and errors to console +logger.warn.log = console.warn.bind(console) +logger.error.log = console.error.bind(console) + export default logger