Skip to content

Commit

Permalink
Merge pull request #32 from safeinsights/add-role-auth
Browse files Browse the repository at this point in the history
Add role auth
  • Loading branch information
therealmarv authored Nov 19, 2024
2 parents 55e083b + 2dbc39f commit 82de4fe
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 26 deletions.
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -78,6 +79,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",
Expand Down
19 changes: 19 additions & 0 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import debug from 'debug'

const logger = {
debug: debug('app:debug'),
info: debug('app:info'),
warn: debug('app:warn'),
error: debug('app:error'),
}

// Enable debug output in development
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
125 changes: 99 additions & 26 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,117 @@
'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'
import debug from 'debug'

const middlewareDebug = debug('app:middleware')

/**
* 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_SLUG = 'openstax'
const SAFEINSIGHTS_ORG_SLUG = 'safe-insights'

// 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, orgSlug } = await auth()

if (!userId) {
// 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
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: orgSlug === SAFEINSIGHTS_ORG_SLUG,
isOpenStaxMember: orgSlug === OPENSTAX_ORG_SLUG,
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' })
)
middlewareDebug('Auth check: %o', {
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')
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) {
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)',
],
}

0 comments on commit 82de4fe

Please sign in to comment.