-
Notifications
You must be signed in to change notification settings - Fork 0
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
Add role auth #32
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
3ba8cc1
restrict certain routes attempt
therealmarv df1f324
get organisations
therealmarv 2e0a869
close match to si_member verification
therealmarv 9168d06
getting closer of finding membership in role
therealmarv f588c7e
checking role works finally
therealmarv d500ad5
test out middleware approach
therealmarv 6c34903
middleware changes tests
therealmarv 1ffd197
enforce organisation selection on login
therealmarv df14079
change org to Openstax
therealmarv c4225f1
protect routes and automatically redirect after login
therealmarv dd5b6e0
fix up logic
therealmarv a512aff
refactor code
therealmarv f7dbd87
remove org selector for now
therealmarv fd67ab3
resolve merge conflicts
therealmarv 9b3b76d
revert changes in main page
therealmarv 92a2221
use a simple logger for debug info of middleware
therealmarv 9294290
add Clerk auth() response example as TypeDoc comment
therealmarv ec2716a
make everything nice
therealmarv df00956
fix type definitons
therealmarv 526b80d
prettier
therealmarv 838b5bd
tidy up naming
therealmarv 996545a
use package debug for logging
therealmarv 326ce34
replace org IDs with slugs
therealmarv 2dbc39f
simplify logger
therealmarv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)', | ||
], | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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