Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add role auth #32

Merged
merged 24 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, but maybe we pull in a "real" logging library that allows filtering and prefixes? I've used https://www.npmjs.com/package/debug before and liked it's functionality

119 changes: 93 additions & 26 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,111 @@
'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(.*)'])
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'
const SAFEINSIGHTS_ORG_ID = 'org_2oUWxfZ5UDD2tZVwRmMF8BpD2rD'

// Clerk middleware reference
// https://clerk.com/docs/references/nextjs/clerk-middleware

export default clerkMiddleware((auth, req) => {
// 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')) {
const { userId } = auth()
if (userId) {
return NextResponse.redirect(new URL('/', req.url))
export default clerkMiddleware(async (auth: any, 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()
}
}

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' })
)
})
// Define user roles
const userRoles = {
isAdmin: orgId === SAFEINSIGHTS_ORG_ID,
isOpenStaxMember: orgId === OPENSTAX_ORG_ID,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use org slugs vs ids. The ids will change across clerk instances and if we ever accidentally destroy an org and recreate it later, but we can control the slugs so they'd stay the same.

get isMember() {
return this.isOpenStaxMember && !this.isAdmin
},
get isResearcher() {
return !this.isAdmin && !this.isOpenStaxMember
},
}

if (isResearcherRoute(req))
auth().protect((has) => {
return (
// TODO setup groups and perms, check them here
has({ permission: 'org:researcher' })
)
logger.info('Middleware:', {
organization: orgId,
role: orgRole,
...userRoles,
})

// Handle authentication redirects
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.isMember && !userRoles.isAdmin,
researcher: isResearcherRoute(req) && !userRoles.isResearcher && !userRoles.isAdmin,
}

if (routeProtection.member) {
logger.warn('Access denied: Member route requires 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) {
logger.error('Middleware error:', error)
}

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
'/(dl|member|researcher)(.*)',
'/',
'/(reset-password|signup)',
],
}