') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(nextjs,shared,backend,clerk-react): Introduce Protect for authorization by panteliselef · Pull Request #2170 · clerk/javascript · GitHub
Skip to content

feat(nextjs,shared,backend,clerk-react): Introduce Protect for authorization - #2170

Merged
panteliselef merged 26 commits into
mainfrom
elef/core-810-gate-with-permissions
Dec 11, 2023
Merged

feat(nextjs,shared,backend,clerk-react): Introduce Protect for authorization#2170
panteliselef merged 26 commits into
mainfrom
elef/core-810-gate-with-permissions

Conversation

@panteliselef

@panteliselefpanteliselef commented Nov 20, 2023

Copy link
Copy Markdown
Contributor

Description

This PR builds upon the Experimental__Gate and experimental__has there introduced recently.

  • Rename Gate to Protect
  • Support for permission checks. (Previously only roles could be used)
  • Remove the experimental tags and prefixes
  • Drop some from the has utility and Protect. Protect now accepts a condition prop where a function is expected with the has being exposed as the param.
  • Protect can now be used without required props. In this case behaves as <SignedIn>, if no authorization props are passed.
  • has will throw an error if neither permission or role is passed.
  • Introduce auth().protect() for App Router.

auth().protect()

Allow per page protection in app router. This utility will automatically throw a 404 error if user is not authorized or authenticated.
When auth().protect() is called

  • inside a page or layout file it will render the nearest not-found component set by the developer
  • inside a route handler it will return empty response body with a 404 status code

Examples

RSC in Nextjs

import{Protect}from'@clerk/nextjs';<Protectpermission="org:appointment:accept"><button>Accept appointment</button></Protect><Protectcondition={has=>has({permission: 'org:appointment:accept'})||has({permission: 'org:appointment:decline'})}><button>Accept appointment</button><button>Declineappointment</button>
</Protect>

Client component

"use client"/** * As a client component * The client component is exposed also from `@clerk/clerk-react */import{Protect}from'@clerk/nextjs';<Protectpermission="org:appointment:accept"><button>Accept appointment</button></Protect><Protectcondition={has=>has({permission: 'org:appointment:accept'})||has({permission: 'org:appointment:decline'})}><button>Accept appointment</button><button>Declineappointment</button>
</Protect>

has from the Auth object

import{auth}from'@clerk/nextjs';constisAuthorized=auth().has({permission: "org:appointment:decline"})import{getAuth}from'@clerk/remix';constisAuthorized=getAuth().has({permission: "org:appointment:decline"})

protect from auth() only for App Router

import{auth}from'@clerk/nextjs';const{userId, ...restAuthObj}=auth().protect()// ^ userId is stringconst{userId, ...restAuthObj}=auth().protect({permission: "org:appointment:decline"})const{userId, ...restAuthObj}=auth().protect({role: "org:admin"})const{userId, ...restAuthObj}=auth().protect(has=>has({permission: "org:appointment:decline"}))

Type-safety for custom roles and permissions

Create a clerk.d.ts file and replace ClerkAuthorization with your own types

// clerk.d.tsinterfaceClerkAuthorization{role: 'org:admin'|'org:editor'|'org:viewer';permission: 'org:article:create'|'org:article:manage'|'org:article:publish';}

If the file is a module, you can do this

// clerk.d.tsdeclare global {interfaceClerkAuthorization{role: 'org:admin'|'org:editor'|'org:viewer';permission: 'org:article:create'|'org:article:manage'|'org:article:publish';}}export{};

Checklist

  • npm test runs as expected.
  • npm run build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Packages affected

  • @clerk/backend
  • @clerk/chrome-extension
  • @clerk/clerk-js
  • @clerk/clerk-expo
  • @clerk/fastify
  • gatsby-plugin-clerk
  • @clerk/localizations
  • @clerk/nextjs
  • @clerk/clerk-react
  • @clerk/remix
  • @clerk/clerk-sdk-node
  • @clerk/shared
  • @clerk/themes
  • @clerk/types
  • build/tooling/chore

@panteliselefpanteliselef self-assigned this Nov 20, 2023
@changeset-bot

changeset-botBot commented Nov 20, 2023

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 79fff5a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
NameType
@clerk/chrome-extensionMinor
@clerk/clerk-jsMinor
@clerk/backendMinor
@clerk/nextjsMinor
@clerk/clerk-reactMinor
@clerk/typesMinor
@clerk/clerk-expoPatch
@clerk/fastifyPatch
gatsby-plugin-clerkPatch
@clerk/remixPatch
@clerk/clerk-sdk-nodePatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread.changeset/chatty-beds-doubt.md Outdated
Comment thread.changeset/chatty-beds-doubt.md Outdated
Comment threadpackages/types/src/organizationMembership.ts Outdated
@panteliselef
panteliselef requested review from a team and brkalowNovember 24, 2023 12:21
@panteliselef
panteliselefforce-pushed the elef/core-810-gate-with-permissions branch from 91c3ba3 to 9dfe2b3CompareNovember 27, 2023 11:51
@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

This comment was marked as outdated.

@panteliselef
panteliselefforce-pushed the elef/core-810-gate-with-permissions branch 2 times, most recently from 91bf440 to bda5428CompareDecember 3, 2023 15:01
@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/backend1.0.1-snapshot.vbda5428
@clerk/chrome-extension1.0.1-snapshot.vbda5428
@clerk/clerk-js5.0.1-snapshot.vbda5428
@clerk/clerk-expo1.0.1-snapshot.vbda5428
@clerk/fastify1.0.1-snapshot.vbda5428
gatsby-plugin-clerk5.0.1-snapshot.vbda5428
@clerk/localizations2.0.1-snapshot.vbda5428
@clerk/nextjs5.0.1-snapshot.vbda5428
@clerk/clerk-react5.0.1-snapshot.vbda5428
@clerk/remix4.0.1-snapshot.vbda5428
@clerk/clerk-sdk-node5.0.1-snapshot.vbda5428
@clerk/shared2.0.1-snapshot.vbda5428
@clerk/themes2.0.1-snapshot.vbda5428
@clerk/types4.0.1-snapshot.vbda5428

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/backend

npm i @clerk/backend@1.0.1-snapshot.vbda5428 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@1.0.1-snapshot.vbda5428 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.0.1-snapshot.vbda5428 --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@1.0.1-snapshot.vbda5428 --save-exact

@clerk/fastify

npm i @clerk/fastify@1.0.1-snapshot.vbda5428 --save-exact

gatsby-plugin-clerk

npm i gatsby-plugin-clerk@5.0.1-snapshot.vbda5428 --save-exact

@clerk/localizations

npm i @clerk/localizations@2.0.1-snapshot.vbda5428 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@5.0.1-snapshot.vbda5428 --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.0.1-snapshot.vbda5428 --save-exact

@clerk/remix

npm i @clerk/remix@4.0.1-snapshot.vbda5428 --save-exact

@clerk/clerk-sdk-node

npm i @clerk/clerk-sdk-node@5.0.1-snapshot.vbda5428 --save-exact

@clerk/shared

npm i @clerk/shared@2.0.1-snapshot.vbda5428 --save-exact

@clerk/themes

npm i @clerk/themes@2.0.1-snapshot.vbda5428 --save-exact

@clerk/types

npm i @clerk/types@4.0.1-snapshot.vbda5428 --save-exact

@panteliselefpanteliselef changed the title feat(nextjs,shared,backend,clerk-react): Support permissions in Gatefeat(nextjs,shared,backend,clerk-react): Introduce Protect for authorizationDec 4, 2023
@panteliselef
panteliselefforce-pushed the elef/core-810-gate-with-permissions branch from be85aef to 636a20aCompareDecember 4, 2023 10:00
Comment threadpackages/nextjs/src/app-router/server/auth.ts
noAuthStatusMessage: authAuthHeaderMissing(),
})(buildRequestLike());

(authObject as AuthSignedIn).protect = params => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We talked about supporting redirectUrl as an optional parameter when doing authz checks, but I can't remember if we decided to omit that for the initial implementation 🤔

auth().protect({role: 'admin'},{redirectUrl: '/home'})

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

I think we are skipping this. Especially with something like notAuthorized we wouldn't need to support redirectUrl

Comment threadpackages/nextjs/src/app-router/server/controlComponents.tsx
Comment threadpackages/nextjs/src/app-router/server/controlComponents.tsx Outdated
Comment threadpackages/react/src/errors.ts Outdated
Comment threadpackages/types/src/organizationMembership.ts
Comment threadpackages/types/src/organizationMembership.ts Outdated
Comment threadpackages/backend/src/tokens/authObjects.ts Outdated
Comment thread.changeset/chatty-beds-doubt.md Outdated
LekoArts
LekoArts previously requested changes Dec 5, 2023

@LekoArtsLekoArts left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Public API-wise I'd like to talk about this before we merge it (hence the request changes to block it):

<Protectcondition={has=>has({"org:appointment:accept"})||has({"org:appointment:decline"})}>

This feels inconsistent. It would need to be either of those two things:

  1. Like the <Protect permission="foobar"> prop so without the {}
  2. Like the rest of the has functions so { permission: "foobar" }

I'd prefer 2) as then has would be consistent everywhere

@LekoArts
LekoArts dismissed their stale reviewDecember 6, 2023 07:38

PR description was incorrect, implementation is like 2)

@LekoArtsLekoArts left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good work 👍

Comment threadpackages/nextjs/src/app-router/server/auth.ts Outdated
Comment threadpackages/nextjs/src/app-router/server/controlComponents.tsx Outdated
Comment threadpackages/nextjs/src/app-router/server/controlComponents.tsx Outdated
Comment threadpackages/nextjs/src/app-router/server/controlComponents.tsx Outdated
Comment threadpackages/nextjs/src/app-router/server/controlComponents.tsx Outdated
Comment threadpackages/react/src/components/controlComponents.tsx Outdated
Comment threadpackages/react/src/components/controlComponents.tsx Outdated
Comment threadpackages/react/src/components/controlComponents.tsx Outdated
Comment threadpackages/react/src/components/controlComponents.tsx Outdated
@panteliselef
panteliselefforce-pushed the elef/core-810-gate-with-permissions branch from d4a99be to e07c709CompareDecember 6, 2023 11:36
@panteliselef
panteliselefforce-pushed the elef/core-810-gate-with-permissions branch from 24efeec to 79fff5aCompareDecember 11, 2023 17:30
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Changes detected under the ClerkJS ui directory!

Don't forget to apply the same changes under the /ui.retheme directory:
packages/clerk-js/src/ui/** ➡️ packages/clerk-js/src/ui.retheme/**

Also, you may need to update the following files:

  • packages/localizations/src/en-US.retheme.ts
  • packages/localizations/src/index.retheme.ts
  • packages/types/src/appearance.retheme.ts
  • packages/types/src/clerk.retheme.ts
  • packages/types/src/index.retheme.ts
  • packages/types/src/localization.retheme.ts

@panteliselef

Copy link
Copy Markdown
ContributorAuthor

!snapshot

@clerk-cookie

Copy link
Copy Markdown
Collaborator

Hey @panteliselef - the snapshot version command generated the following package versions:

PackageVersion
@clerk/backend1.0.1-snapshot.v79fff5a
@clerk/chrome-extension1.0.1-snapshot.v79fff5a
@clerk/clerk-js5.0.1-snapshot.v79fff5a
@clerk/clerk-expo1.0.1-snapshot.v79fff5a
@clerk/fastify1.0.1-snapshot.v79fff5a
gatsby-plugin-clerk5.0.1-snapshot.v79fff5a
@clerk/localizations2.0.1-snapshot.v79fff5a
@clerk/nextjs5.0.1-snapshot.v79fff5a
@clerk/clerk-react5.0.1-snapshot.v79fff5a
@clerk/remix4.0.1-snapshot.v79fff5a
@clerk/clerk-sdk-node5.0.1-snapshot.v79fff5a
@clerk/shared2.0.1-snapshot.v79fff5a
@clerk/themes2.0.1-snapshot.v79fff5a
@clerk/types4.0.1-snapshot.v79fff5a

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/backend

npm i @clerk/backend@1.0.1-snapshot.v79fff5a --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@1.0.1-snapshot.v79fff5a --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@5.0.1-snapshot.v79fff5a --save-exact

@clerk/clerk-expo

npm i @clerk/clerk-expo@1.0.1-snapshot.v79fff5a --save-exact

@clerk/fastify

npm i @clerk/fastify@1.0.1-snapshot.v79fff5a --save-exact

gatsby-plugin-clerk

npm i gatsby-plugin-clerk@5.0.1-snapshot.v79fff5a --save-exact

@clerk/localizations

npm i @clerk/localizations@2.0.1-snapshot.v79fff5a --save-exact

@clerk/nextjs

npm i @clerk/nextjs@5.0.1-snapshot.v79fff5a --save-exact

@clerk/clerk-react

npm i @clerk/clerk-react@5.0.1-snapshot.v79fff5a --save-exact

@clerk/remix

npm i @clerk/remix@4.0.1-snapshot.v79fff5a --save-exact

@clerk/clerk-sdk-node

npm i @clerk/clerk-sdk-node@5.0.1-snapshot.v79fff5a --save-exact

@clerk/shared

npm i @clerk/shared@2.0.1-snapshot.v79fff5a --save-exact

@clerk/themes

npm i @clerk/themes@2.0.1-snapshot.v79fff5a --save-exact

@clerk/types

npm i @clerk/types@4.0.1-snapshot.v79fff5a --save-exact

@panteliselef
panteliselef added this pull request to the merge queue Dec 11, 2023
Merged via the queue into main with commit 46040a2Dec 11, 2023
@panteliselef
panteliselef deleted the elef/core-810-gate-with-permissions branch December 11, 2023 17:52
github-merge-queueBot pushed a commit that referenced this pull request Dec 12, 2023
…ization (#2170) (#2309)
* feat(nextjs,shared,backend,clerk-react): Introduce Protect for authorization (#2170)
* fix(types): Avoid using `ts-expect-error` as it may fail for hosting apps
* fix(types,nextjs): Improve complex type
* fix(types): Typescript v5 cannot infer types correctly
* fix(types): Update MembershipRole and OrganizationPermissionKey to not resolve to `any`
octoper pushed a commit that referenced this pull request Dec 13, 2023
…ization (#2170)
* feat(nextjs,shared,backend,clerk-react): Support permissions in Gate
* chore(types,backend,clerk-react): Create type for OrganizationCustomPermissions
* chore(types,backend,clerk-react): Create type for custom roles
* chore(types,backend,clerk-react): Add changeset
* chore(types,backend,clerk-react): Add comments
* chore(types,nextjs): Remove custom types
* fix(clerk-react): Missing `some` support for has in useAuth
* chore(types,clerk-react): Use OrganizationCustomPermission for permissions in ssr
* chore(nextjs): Drop redirect from RSC `<Gate/>`
* feat(types,nextjs,clerk-react,backend): Rename Gate to Protect
- Drop `some` from the `has` utility and Protect. Protect now accepts a `condition` prop where a function is expected with the `has` being exposed as the param.
- Protect can now be used without required props. In this chae behaves as `<SignedIn>` if no authorization props are passed.
- `has` will throw an error if neither `permission` or `role` is passed.
* feat(nextjs): Introduce `auth().protect()` for App Router
Allow per page protection in app router. This utility will automatically throw a 404 error if user is not authorized or authenticated.
When `auth().protect()` is called
- inside a page or layout file it will render the nearest `not-found` component set by the developer
- inside a route handler it will return empty response body with a 404 status code
* chore(types): Add `Key` prefix to OrganizationCustomPermission
* chore(nextjs): Remove duplicate types
* chore(nextjs): Minor improvements in readability
* chore(nextjs): Mark protect utility as experimental for Nextjs
* chore(nextjs): Minor improvements
* fix(nextjs,clerk-react,backend): Utility `has` is undefined when user is signed out
* fix(clerk-react): Utility `has` returns false when user isLoaded is true and no user or org
* chore(clerk-react,nextjs): Improve comments
* fix(clerk-react): Eliminate flickering of fallback for CSR applications
* feat(types): Allow overriding of types for custom roles and permissions
* chore(repo): Update changeset file
* fix(types): `MembershipRole` will include custom roles if applicable
* chore(nextjs): Improve readability of conditionals
* Revert "fix(nextjs,clerk-react,backend): Utility `has` is undefined when user is signed out"
This reverts commit cf736cc
* fix(clerk-js,types): Remove `experimental` from checkAuthorization
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@panteliselef@clerk-cookie@nikosdouvlis@brkalow@SokratisVidros@LekoArts