From 973aa9762b2e70ef7bc183c9224911fec38093f0 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 3 Jan 2024 17:14:13 +0000 Subject: [PATCH 01/58] Add universal required email config for authentication --- docs/schemas/v1/definitions.json | 7 + packages/toolpad-app/src/appDom/index.ts | 1 + .../toolpad-app/src/runtime/AppLayout.tsx | 3 + .../toolpad-app/src/runtime/ToolpadApp.tsx | 4 +- packages/toolpad-app/src/runtime/useAuth.ts | 11 +- packages/toolpad-app/src/server/auth.ts | 79 ++++- packages/toolpad-app/src/server/index.ts | 2 +- packages/toolpad-app/src/server/schema.ts | 4 + .../src/server/toolpadAppServer.ts | 2 +- .../AppEditor/AppAuthorizationEditor.tsx | 300 ++++++++++++------ packages/toolpad-app/src/toolpad/Toolpad.tsx | 2 +- packages/toolpad-app/typings/@auth.d.ts | 5 + 12 files changed, 298 insertions(+), 122 deletions(-) create mode 100644 packages/toolpad-app/typings/@auth.d.ts diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index e85eaf0d061..d64d1e81e88 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -40,6 +40,13 @@ "additionalProperties": false }, "description": "Authentication providers to use." + }, + "requiredEmail": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Valid email patterns for the authenticated user." } }, "additionalProperties": false, diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index fc77c90008d..d44a331a9c9 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -59,6 +59,7 @@ export interface AppNode extends AppDomNodeBase { readonly attributes: { readonly authentication?: { readonly providers?: AuthProviderConfig[]; + readonly requiredEmail?: string[]; }; readonly authorization?: { readonly roles?: { diff --git a/packages/toolpad-app/src/runtime/AppLayout.tsx b/packages/toolpad-app/src/runtime/AppLayout.tsx index d86ca02b0e1..ea8ecea86ad 100644 --- a/packages/toolpad-app/src/runtime/AppLayout.tsx +++ b/packages/toolpad-app/src/runtime/AppLayout.tsx @@ -225,6 +225,9 @@ export function AppLayout({ ); - if (!IS_RENDERED_IN_CANVAS && hasAuthentication && page.attributes.authorization) { + if (!IS_RENDERED_IN_CANVAS && hasAuthentication) { pageContent = ( {pageContent} diff --git a/packages/toolpad-app/src/runtime/useAuth.ts b/packages/toolpad-app/src/runtime/useAuth.ts index fa6d0830815..72454802146 100644 --- a/packages/toolpad-app/src/runtime/useAuth.ts +++ b/packages/toolpad-app/src/runtime/useAuth.ts @@ -69,11 +69,16 @@ export function useAuth({ dom, basename }: UseAuthInput): AuthPayload { }, [basename]); const signOut = React.useCallback(async () => { - try { - setIsSigningOut(true); + setIsSigningOut(true); - const csrfToken = await getCsrfToken(); + let csrfToken; + try { + csrfToken = await getCsrfToken(); + } catch (error) { + console.error((error as Error).message); + } + try { await fetch(`${basename}${AUTH_SIGNOUT_PATH}`, { method: 'POST', headers: { diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index dfd098c5c6b..dd1b8710155 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -1,14 +1,18 @@ import express, { Router } from 'express'; import { Auth } from '@auth/core'; -import GithubProvider from '@auth/core/providers/github'; +import GithubProvider, { GitHubEmail, GitHubProfile } from '@auth/core/providers/github'; import GoogleProvider from '@auth/core/providers/google'; import { getToken } from '@auth/core/jwt'; +import { TokenSet } from '@auth/core/types'; +import { OAuthConfig } from '@auth/core/providers'; import { asyncHandler } from '../utils/express'; import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; import { ToolpadProject } from './localMode'; import * as appDom from '../appDom'; -export function createAuthHandler(base: string): Router { +export function createAuthHandler(project: ToolpadProject): Router { + const { base } = project.options; + const router = express.Router(); router.use( @@ -27,6 +31,42 @@ export function createAuthHandler(base: string): Router { GithubProvider({ clientId: process.env.TOOLPAD_GITHUB_ID, clientSecret: process.env.TOOLPAD_GITHUB_SECRET, + userinfo: { + url: 'https://api.github.com/user', + async request({ + tokens, + provider, + }: { + tokens: TokenSet; + provider: OAuthConfig; + }) { + const profile = await fetch(provider.userinfo?.url as URL, { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + 'User-Agent': 'authjs', + }, + }).then(async (githubRes) => githubRes.json()); + + if (!profile.email) { + // If the user does not have a public email, get another via the GitHub API + // See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user + const githubRes = await fetch('https://api.github.com/user/emails', { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + 'User-Agent': 'authjs', + }, + }); + + if (githubRes.ok) { + const emails: GitHubEmail[] = await githubRes.json(); + profile.email = (emails.find((e) => e.primary) ?? emails[0]).email; + profile.verifiedEmails = emails.filter((e) => e.verified).map((e) => e.email); + } + } + + return profile; + }, + }, }), GoogleProvider({ clientId: process.env.TOOLPAD_GOOGLE_CLIENT_ID, @@ -44,12 +84,30 @@ export function createAuthHandler(base: string): Router { trustHost: true, callbacks: { async signIn({ account, profile }) { + const dom = await project.loadDom(); + + const app = appDom.getApp(dom); + const requiredEmails = app.attributes.authentication?.requiredEmail ?? []; + + if (account?.provider === 'github') { + return Boolean( + profile?.verifiedEmails && + (requiredEmails.length === 0 || + requiredEmails.some((requiredEmail) => + profile.verifiedEmails!.some((verifiedEmail) => + new RegExp(requiredEmail).test(verifiedEmail), + ), + )), + ); + } if (account?.provider === 'google') { return Boolean( profile?.email_verified && profile?.email && - (!process.env.TOOLPAD_GOOGLE_AUTH_DOMAIN || - profile.email.endsWith(`@${process.env.TOOLPAD_GOOGLE_AUTH_DOMAIN}`)), + (requiredEmails.length === 0 || + requiredEmails.some( + (requiredEmail) => new RegExp(requiredEmail).test(profile.email!) ?? false, + )), ); } return true; @@ -91,10 +149,11 @@ export async function createAuthPagesMiddleware(project: ToolpadProject) { const signInPath = `${base}/signin`; + let isRedirect = false; if ( hasAuthentication && req.get('sec-fetch-dest') === 'document' && - req.originalUrl !== signInPath && + req.originalUrl.split('?')[0] !== signInPath && !req.originalUrl.startsWith(`${base}/api/auth`) ) { const request = adaptRequestFromExpressToFetch(req); @@ -111,11 +170,13 @@ export async function createAuthPagesMiddleware(project: ToolpadProject) { } if (!token) { - res.redirect(signInPath); - res.end(); - } else { - next(); + isRedirect = true; } + } + + if (isRedirect) { + res.redirect(signInPath); + res.end(); } else { next(); } diff --git a/packages/toolpad-app/src/server/index.ts b/packages/toolpad-app/src/server/index.ts index 59cd181762e..ee1986cb191 100644 --- a/packages/toolpad-app/src/server/index.ts +++ b/packages/toolpad-app/src/server/index.ts @@ -125,7 +125,7 @@ async function createDevHandler(project: ToolpadProject) { handler.use('/api/runtime-rpc', createRpcHandler(runtimeRpcServer)); if (process.env.TOOLPAD_AUTH_SECRET) { - const authHandler = createAuthHandler(project.options.base); + const authHandler = createAuthHandler(project); handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); } diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index df488f6ff59..f4cb8293e38 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -271,6 +271,10 @@ export const applicationSchema = toolpadObjectSchema( ) .optional() .describe('Authentication providers to use.'), + requiredEmail: z + .array(z.string()) + .optional() + .describe('Valid email patterns for the authenticated user.'), }) .optional() .describe('Authentication configuration for this application.'), diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index e14793fffbe..6fcb7eefb3b 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -69,7 +69,7 @@ export async function createProdHandler(project: ToolpadProject) { handler.use('/api/runtime-rpc', createRpcHandler(runtimeRpcServer)); if (process.env.TOOLPAD_AUTH_SECRET) { - const authHandler = createAuthHandler(project.options.base); + const authHandler = createAuthHandler(project); handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); } diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index ff661f2eec6..5a898af51ff 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Alert, + Box, Button, Checkbox, Dialog, @@ -14,7 +15,10 @@ import { MenuItem, Select, SelectChangeEvent, + Snackbar, Stack, + Tab, + TextField, Tooltip, Typography, } from '@mui/material'; @@ -31,21 +35,144 @@ import { } from '@mui/x-data-grid'; import GitHubIcon from '@mui/icons-material/GitHub'; import GoogleIcon from '@mui/icons-material/Google'; +import { TabContext, TabList } from '@mui/lab'; import { useAppState, useAppStateApi } from '../AppState'; import * as appDom from '../../appDom'; +import TabPanel from '../../components/TabPanel'; import { AuthProviderConfig, AuthProvider } from '../../types'; +import { updateArray } from '../../utils/immutability'; const AUTH_PROVIDERS = new Map([ ['github', { name: 'GitHub', Icon: GitHubIcon }], ['google', { name: 'Google', Icon: GoogleIcon }], ]); -interface EditToolbarProps { +export function AppAuthenticationEditor() { + const { dom } = useAppState(); + const appState = useAppStateApi(); + + const handleAuthProvidersChange = React.useCallback( + (event: SelectChangeEvent) => { + const { + target: { value: providers }, + } = event; + + appState.update((draft) => { + const app = appDom.getApp(draft); + + draft = appDom.setNodeNamespacedProp(draft, app, 'attributes', 'authentication', { + ...app.attributes?.authentication, + providers: (typeof providers === 'string' ? providers.split(',') : providers).map( + (provider) => ({ provider } as AuthProviderConfig), + ), + }); + + return draft; + }); + }, + [appState], + ); + + const handleRequiredEmailChange = React.useCallback( + (index: number) => (event: React.ChangeEvent) => { + const { + target: { value: email }, + } = event; + + appState.update((draft) => { + const app = appDom.getApp(draft); + + draft = appDom.setNodeNamespacedProp(draft, app, 'attributes', 'authentication', { + ...app.attributes?.authentication, + requiredEmail: updateArray( + app.attributes?.authentication?.requiredEmail ?? [], + email, + index, + ).filter((requiredEmail) => requiredEmail !== ''), + }); + + return draft; + }); + }, + [appState], + ); + + const appNode = appDom.getApp(dom); + const { authentication } = appNode.attributes; + + const authProviders = React.useMemo( + () => authentication?.providers ?? [], + [authentication?.providers], + ).map((providerConfig) => providerConfig.provider); + + const requiredEmails = authentication?.requiredEmail ?? []; + + return ( + + + Providers + + + Authentication providers + + labelId="auth-providers-label" + label="Authentication providers" + id="auth-providers" + multiple + value={authProviders} + onChange={handleAuthProvidersChange} + fullWidth + renderValue={(selected) => + selected + .map((selectedValue) => AUTH_PROVIDERS.get(selectedValue)?.name ?? '') + .join(', ') + } + > + {[...AUTH_PROVIDERS].map(([value, { name, Icon }]) => ( + + + -1} /> + + {name} + + + ))} + + + If set, only authenticated users can use the app. + + + + Certain environment variables must be set for authentication providers to work.{' '} + + Learn how to set up authentication + + . + + + Required email patterns + + + If set, authenticated user emails must match one of the patterns below. + + {[...requiredEmails, ''].map((email, index) => ( + + ))} + + ); +} + +interface RolesToolbarProps { addNewRoleDisabled: boolean; onAddNewRole: () => void; } -function EditToolbar({ addNewRoleDisabled, onAddNewRole }: EditToolbarProps) { +function RolesToolbar({ addNewRoleDisabled, onAddNewRole }: RolesToolbarProps) { return ( - - + + + Authorization + + + + + + + + + + + + + + Define the roles for your application. You can configure your pages to be accessible + to specific roles only. + + + + + + + + + + + {errorSnackbarMessage ? ( + + {errorSnackbarMessage} + + ) : undefined} + + ); } diff --git a/packages/toolpad-app/src/toolpad/Toolpad.tsx b/packages/toolpad-app/src/toolpad/Toolpad.tsx index 13cb4612d13..159aa33765c 100644 --- a/packages/toolpad-app/src/toolpad/Toolpad.tsx +++ b/packages/toolpad-app/src/toolpad/Toolpad.tsx @@ -16,7 +16,7 @@ import { getViewFromPathname } from '../utils/domView'; import AppProvider, { AppState, useAppStateContext } from './AppState'; import { FEATURE_FLAG_AUTHORIZATION, FEATURE_FLAG_GLOBAL_FUNCTIONS } from '../constants'; import { ProjectProvider } from '../project'; -import { AppAuthorizationDialog } from './AppEditor/AppAuthorizationEditor'; +import AppAuthorizationDialog from './AppEditor/AppAuthorizationEditor'; import useBoolean from '../utils/useBoolean'; const Centered = styled('div')({ diff --git a/packages/toolpad-app/typings/@auth.d.ts b/packages/toolpad-app/typings/@auth.d.ts new file mode 100644 index 00000000000..e28ef3fb444 --- /dev/null +++ b/packages/toolpad-app/typings/@auth.d.ts @@ -0,0 +1,5 @@ +export declare module '@auth/core/types' { + interface Profile { + verifiedEmails?: string[]; + } +} From ba2399d6497ad81d2c31d21458d212bf9b673239 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 3 Jan 2024 17:17:27 +0000 Subject: [PATCH 02/58] Must have at least 1 verified email in Github --- packages/toolpad-app/src/server/auth.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index dd1b8710155..bfda720d71d 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -92,6 +92,7 @@ export function createAuthHandler(project: ToolpadProject): Router { if (account?.provider === 'github') { return Boolean( profile?.verifiedEmails && + profile.verifiedEmails.length > 0 && (requiredEmails.length === 0 || requiredEmails.some((requiredEmail) => profile.verifiedEmails!.some((verifiedEmail) => From 08791c6ebc46e410ce33548447e0f46f303896e1 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:57:16 +0000 Subject: [PATCH 03/58] Address review comments --- docs/schemas/v1/definitions.json | 2 +- packages/toolpad-app/src/appDom/index.ts | 2 +- packages/toolpad-app/src/server/auth.ts | 14 ++++++------- packages/toolpad-app/src/server/schema.ts | 2 +- .../AppEditor/AppAuthorizationEditor.tsx | 20 +++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index d64d1e81e88..0ba1ffcef93 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -41,7 +41,7 @@ }, "description": "Authentication providers to use." }, - "requiredEmail": { + "requiredDomain": { "type": "array", "items": { "type": "string" diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index d44a331a9c9..4b70c84d51e 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -59,7 +59,7 @@ export interface AppNode extends AppDomNodeBase { readonly attributes: { readonly authentication?: { readonly providers?: AuthProviderConfig[]; - readonly requiredEmail?: string[]; + readonly requiredDomain?: string[]; }; readonly authorization?: { readonly roles?: { diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index bfda720d71d..48278866543 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -87,16 +87,16 @@ export function createAuthHandler(project: ToolpadProject): Router { const dom = await project.loadDom(); const app = appDom.getApp(dom); - const requiredEmails = app.attributes.authentication?.requiredEmail ?? []; + const requiredDomains = app.attributes.authentication?.requiredDomain ?? []; if (account?.provider === 'github') { return Boolean( profile?.verifiedEmails && profile.verifiedEmails.length > 0 && - (requiredEmails.length === 0 || - requiredEmails.some((requiredEmail) => + (requiredDomains.length === 0 || + requiredDomains.some((requiredDomain) => profile.verifiedEmails!.some((verifiedEmail) => - new RegExp(requiredEmail).test(verifiedEmail), + verifiedEmail.endsWith(`@${requiredDomain}`), ), )), ); @@ -105,9 +105,9 @@ export function createAuthHandler(project: ToolpadProject): Router { return Boolean( profile?.email_verified && profile?.email && - (requiredEmails.length === 0 || - requiredEmails.some( - (requiredEmail) => new RegExp(requiredEmail).test(profile.email!) ?? false, + (requiredDomains.length === 0 || + requiredDomains.some( + (requiredDomain) => profile.email!.endsWith(`@${requiredDomain}`) ?? false, )), ); } diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index f4cb8293e38..ade8997643d 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -271,7 +271,7 @@ export const applicationSchema = toolpadObjectSchema( ) .optional() .describe('Authentication providers to use.'), - requiredEmail: z + requiredDomain: z .array(z.string()) .optional() .describe('Valid email patterns for the authenticated user.'), diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 5a898af51ff..5e1cc936247 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -73,7 +73,7 @@ export function AppAuthenticationEditor() { [appState], ); - const handleRequiredEmailChange = React.useCallback( + const handleRequiredDomainChange = React.useCallback( (index: number) => (event: React.ChangeEvent) => { const { target: { value: email }, @@ -84,11 +84,11 @@ export function AppAuthenticationEditor() { draft = appDom.setNodeNamespacedProp(draft, app, 'attributes', 'authentication', { ...app.attributes?.authentication, - requiredEmail: updateArray( - app.attributes?.authentication?.requiredEmail ?? [], + requiredDomain: updateArray( + app.attributes?.authentication?.requiredDomain ?? [], email, index, - ).filter((requiredEmail) => requiredEmail !== ''), + ).filter((requiredDomain) => requiredDomain !== ''), }); return draft; @@ -105,7 +105,7 @@ export function AppAuthenticationEditor() { [authentication?.providers], ).map((providerConfig) => providerConfig.provider); - const requiredEmails = authentication?.requiredEmail ?? []; + const requiredDomains = authentication?.requiredDomain ?? []; return ( @@ -150,17 +150,17 @@ export function AppAuthenticationEditor() { . - Required email patterns + Required email domains - If set, authenticated user emails must match one of the patterns below. + If set, authenticated user emails must be in one of the domains below. - {[...requiredEmails, ''].map((email, index) => ( + {[...requiredDomains, ''].map((email, index) => ( ))} From 39c0f006c403dd400f6d9611a81cd8766b29a422 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:10:58 +0000 Subject: [PATCH 04/58] Refactor (review comments) --- docs/schemas/v1/definitions.json | 2 +- packages/toolpad-app/src/appDom/index.ts | 2 +- packages/toolpad-app/src/server/auth.ts | 203 +++++++++--------- packages/toolpad-app/src/server/schema.ts | 2 +- .../AppEditor/AppAuthorizationEditor.tsx | 20 +- 5 files changed, 117 insertions(+), 112 deletions(-) diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index 0ba1ffcef93..4196e62bef1 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -41,7 +41,7 @@ }, "description": "Authentication providers to use." }, - "requiredDomain": { + "restrictedDomains": { "type": "array", "items": { "type": "string" diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index 4b70c84d51e..c3d9b0b05e3 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -59,7 +59,7 @@ export interface AppNode extends AppDomNodeBase { readonly attributes: { readonly authentication?: { readonly providers?: AuthProviderConfig[]; - readonly requiredDomain?: string[]; + readonly restrictedDomains?: string[]; }; readonly authorization?: { readonly roles?: { diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 48278866543..2197f772185 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -3,7 +3,7 @@ import { Auth } from '@auth/core'; import GithubProvider, { GitHubEmail, GitHubProfile } from '@auth/core/providers/github'; import GoogleProvider from '@auth/core/providers/google'; import { getToken } from '@auth/core/jwt'; -import { TokenSet } from '@auth/core/types'; +import { AuthConfig, TokenSet } from '@auth/core/types'; import { OAuthConfig } from '@auth/core/providers'; import { asyncHandler } from '../utils/express'; import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; @@ -15,109 +15,114 @@ export function createAuthHandler(project: ToolpadProject): Router { const router = express.Router(); + const githubProvider = GithubProvider({ + clientId: process.env.TOOLPAD_GITHUB_ID, + clientSecret: process.env.TOOLPAD_GITHUB_SECRET, + userinfo: { + url: 'https://api.github.com/user', + async request({ + tokens, + provider, + }: { + tokens: TokenSet; + provider: OAuthConfig; + }) { + const profile = await fetch(provider.userinfo?.url as URL, { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + 'User-Agent': 'authjs', + }, + }).then(async (githubRes) => githubRes.json()); + + if (!profile.email) { + // If the user does not have a public email, get another via the GitHub API + // See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user + const githubRes = await fetch('https://api.github.com/user/emails', { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + 'User-Agent': 'authjs', + }, + }); + + if (githubRes.ok) { + const emails: GitHubEmail[] = await githubRes.json(); + profile.email = (emails.find((e) => e.primary) ?? emails[0]).email; + profile.verifiedEmails = emails.filter((e) => e.verified).map((e) => e.email); + } + } + + return profile; + }, + }, + }); + + const googleProvider = GoogleProvider({ + clientId: process.env.TOOLPAD_GOOGLE_CLIENT_ID, + clientSecret: process.env.TOOLPAD_GOOGLE_CLIENT_SECRET, + authorization: { + params: { + prompt: 'consent', + access_type: 'offline', + response_type: 'code', + }, + }, + }); + + const authConfig: AuthConfig = { + pages: { + signIn: `${base}/signin`, + signOut: base, + error: `${base}/signin`, // Error code passed in query string as ?error= + verifyRequest: base, + }, + providers: [githubProvider, googleProvider], + secret: process.env.TOOLPAD_AUTH_SECRET, + trustHost: true, + callbacks: { + async signIn({ account, profile }) { + const dom = await project.loadDom(); + const app = appDom.getApp(dom); + + const restrictedDomains = app.attributes.authentication?.restrictedDomains ?? []; + + if (account?.provider === 'github') { + return Boolean( + profile?.verifiedEmails && + profile.verifiedEmails.length > 0 && + (restrictedDomains.length === 0 || + restrictedDomains.some((restrictedDomain) => + profile.verifiedEmails!.some((verifiedEmail) => + verifiedEmail.endsWith(`@${restrictedDomain}`), + ), + )), + ); + } + + if (account?.provider === 'google') { + return Boolean( + profile?.email_verified && + profile?.email && + (restrictedDomains.length === 0 || + restrictedDomains.some( + (restrictedDomain) => profile.email!.endsWith(`@${restrictedDomain}`) ?? false, + )), + ); + } + + return true; + }, + async redirect({ baseUrl }) { + return `${baseUrl}${base}`; + }, + }, + }; + router.use( '/*', asyncHandler(async (req, res) => { const request = adaptRequestFromExpressToFetch(req); - const response = (await Auth(request, { - pages: { - signIn: `${base}/signin`, - signOut: base, - error: `${base}/signin`, // Error code passed in query string as ?error= - verifyRequest: base, - }, - providers: [ - GithubProvider({ - clientId: process.env.TOOLPAD_GITHUB_ID, - clientSecret: process.env.TOOLPAD_GITHUB_SECRET, - userinfo: { - url: 'https://api.github.com/user', - async request({ - tokens, - provider, - }: { - tokens: TokenSet; - provider: OAuthConfig; - }) { - const profile = await fetch(provider.userinfo?.url as URL, { - headers: { - Authorization: `Bearer ${tokens.access_token}`, - 'User-Agent': 'authjs', - }, - }).then(async (githubRes) => githubRes.json()); - - if (!profile.email) { - // If the user does not have a public email, get another via the GitHub API - // See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user - const githubRes = await fetch('https://api.github.com/user/emails', { - headers: { - Authorization: `Bearer ${tokens.access_token}`, - 'User-Agent': 'authjs', - }, - }); - - if (githubRes.ok) { - const emails: GitHubEmail[] = await githubRes.json(); - profile.email = (emails.find((e) => e.primary) ?? emails[0]).email; - profile.verifiedEmails = emails.filter((e) => e.verified).map((e) => e.email); - } - } - - return profile; - }, - }, - }), - GoogleProvider({ - clientId: process.env.TOOLPAD_GOOGLE_CLIENT_ID, - clientSecret: process.env.TOOLPAD_GOOGLE_CLIENT_SECRET, - authorization: { - params: { - prompt: 'consent', - access_type: 'offline', - response_type: 'code', - }, - }, - }), - ], - secret: process.env.TOOLPAD_AUTH_SECRET, - trustHost: true, - callbacks: { - async signIn({ account, profile }) { - const dom = await project.loadDom(); - - const app = appDom.getApp(dom); - const requiredDomains = app.attributes.authentication?.requiredDomain ?? []; - - if (account?.provider === 'github') { - return Boolean( - profile?.verifiedEmails && - profile.verifiedEmails.length > 0 && - (requiredDomains.length === 0 || - requiredDomains.some((requiredDomain) => - profile.verifiedEmails!.some((verifiedEmail) => - verifiedEmail.endsWith(`@${requiredDomain}`), - ), - )), - ); - } - if (account?.provider === 'google') { - return Boolean( - profile?.email_verified && - profile?.email && - (requiredDomains.length === 0 || - requiredDomains.some( - (requiredDomain) => profile.email!.endsWith(`@${requiredDomain}`) ?? false, - )), - ); - } - return true; - }, - async redirect({ baseUrl }) { - return `${baseUrl}${base}`; - }, - }, - })) as Response; + const response = (await Auth(request, authConfig)) as Response; // Converting Fetch API's Response to Express' res res.status(response.status); diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index ade8997643d..cd58c14ce91 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -271,7 +271,7 @@ export const applicationSchema = toolpadObjectSchema( ) .optional() .describe('Authentication providers to use.'), - requiredDomain: z + restrictedDomains: z .array(z.string()) .optional() .describe('Valid email patterns for the authenticated user.'), diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 5e1cc936247..5fc4fb06a85 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -73,10 +73,10 @@ export function AppAuthenticationEditor() { [appState], ); - const handleRequiredDomainChange = React.useCallback( + const handleRestrictedDomainsChange = React.useCallback( (index: number) => (event: React.ChangeEvent) => { const { - target: { value: email }, + target: { value: domain }, } = event; appState.update((draft) => { @@ -84,11 +84,11 @@ export function AppAuthenticationEditor() { draft = appDom.setNodeNamespacedProp(draft, app, 'attributes', 'authentication', { ...app.attributes?.authentication, - requiredDomain: updateArray( - app.attributes?.authentication?.requiredDomain ?? [], - email, + restrictedDomains: updateArray( + app.attributes?.authentication?.restrictedDomains ?? [], + domain, index, - ).filter((requiredDomain) => requiredDomain !== ''), + ).filter((restrictedDomain) => restrictedDomain !== ''), }); return draft; @@ -105,7 +105,7 @@ export function AppAuthenticationEditor() { [authentication?.providers], ).map((providerConfig) => providerConfig.provider); - const requiredDomains = authentication?.requiredDomain ?? []; + const restrictedDomains = authentication?.restrictedDomains ?? []; return ( @@ -155,11 +155,11 @@ export function AppAuthenticationEditor() { If set, authenticated user emails must be in one of the domains below. - {[...requiredDomains, ''].map((email, index) => ( + {[...restrictedDomains, ''].map((domain, index) => ( ))} From d7fc87ca80e84c8397256a90dd14199a600465ef Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 5 Jan 2024 17:40:54 +0000 Subject: [PATCH 05/58] Small refactor --- packages/toolpad-app/src/server/auth.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 2197f772185..043b08545c5 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -27,21 +27,20 @@ export function createAuthHandler(project: ToolpadProject): Router { tokens: TokenSet; provider: OAuthConfig; }) { + const headers = { + Authorization: `Bearer ${tokens.access_token}`, + 'User-Agent': 'authjs', + }; + const profile = await fetch(provider.userinfo?.url as URL, { - headers: { - Authorization: `Bearer ${tokens.access_token}`, - 'User-Agent': 'authjs', - }, + headers, }).then(async (githubRes) => githubRes.json()); if (!profile.email) { // If the user does not have a public email, get another via the GitHub API // See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user const githubRes = await fetch('https://api.github.com/user/emails', { - headers: { - Authorization: `Bearer ${tokens.access_token}`, - 'User-Agent': 'authjs', - }, + headers, }); if (githubRes.ok) { From 07015a045461e47395b092807171c3aec9b42277 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:20:03 +0000 Subject: [PATCH 06/58] Load env before imports --- packages/toolpad-app/src/server/auth.ts | 102 +++++++++--------- .../src/server/toolpadAppServer.ts | 1 + 2 files changed, 52 insertions(+), 51 deletions(-) diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 043b08545c5..ba1ca3e1d0b 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -10,62 +10,62 @@ import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; import { ToolpadProject } from './localMode'; import * as appDom from '../appDom'; -export function createAuthHandler(project: ToolpadProject): Router { - const { base } = project.options; - - const router = express.Router(); - - const githubProvider = GithubProvider({ - clientId: process.env.TOOLPAD_GITHUB_ID, - clientSecret: process.env.TOOLPAD_GITHUB_SECRET, - userinfo: { - url: 'https://api.github.com/user', - async request({ - tokens, - provider, - }: { - tokens: TokenSet; - provider: OAuthConfig; - }) { - const headers = { - Authorization: `Bearer ${tokens.access_token}`, - 'User-Agent': 'authjs', - }; - - const profile = await fetch(provider.userinfo?.url as URL, { +const githubProvider = GithubProvider({ + clientId: process.env.TOOLPAD_GITHUB_ID, + clientSecret: process.env.TOOLPAD_GITHUB_SECRET, + userinfo: { + url: 'https://api.github.com/user', + async request({ + tokens, + provider, + }: { + tokens: TokenSet; + provider: OAuthConfig; + }) { + const headers = { + Authorization: `Bearer ${tokens.access_token}`, + 'User-Agent': 'authjs', + }; + + const profile = await fetch(provider.userinfo?.url as URL, { + headers, + }).then(async (githubRes) => githubRes.json()); + + if (!profile.email) { + // If the user does not have a public email, get another via the GitHub API + // See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user + const githubRes = await fetch('https://api.github.com/user/emails', { headers, - }).then(async (githubRes) => githubRes.json()); - - if (!profile.email) { - // If the user does not have a public email, get another via the GitHub API - // See https://docs.github.com/en/rest/users/emails#list-public-email-addresses-for-the-authenticated-user - const githubRes = await fetch('https://api.github.com/user/emails', { - headers, - }); - - if (githubRes.ok) { - const emails: GitHubEmail[] = await githubRes.json(); - profile.email = (emails.find((e) => e.primary) ?? emails[0]).email; - profile.verifiedEmails = emails.filter((e) => e.verified).map((e) => e.email); - } + }); + + if (githubRes.ok) { + const emails: GitHubEmail[] = await githubRes.json(); + profile.email = (emails.find((e) => e.primary) ?? emails[0]).email; + profile.verifiedEmails = emails.filter((e) => e.verified).map((e) => e.email); } + } - return profile; - }, + return profile; }, - }); - - const googleProvider = GoogleProvider({ - clientId: process.env.TOOLPAD_GOOGLE_CLIENT_ID, - clientSecret: process.env.TOOLPAD_GOOGLE_CLIENT_SECRET, - authorization: { - params: { - prompt: 'consent', - access_type: 'offline', - response_type: 'code', - }, + }, +}); + +const googleProvider = GoogleProvider({ + clientId: process.env.TOOLPAD_GOOGLE_CLIENT_ID, + clientSecret: process.env.TOOLPAD_GOOGLE_CLIENT_SECRET, + authorization: { + params: { + prompt: 'consent', + access_type: 'offline', + response_type: 'code', }, - }); + }, +}); + +export function createAuthHandler(project: ToolpadProject): Router { + const { base } = project.options; + + const router = express.Router(); const authConfig: AuthConfig = { pages: { diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index 6fcb7eefb3b..9aa7bb1ad64 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -1,3 +1,4 @@ +import 'dotenv/config'; import * as path from 'path'; import * as fs from 'fs/promises'; import { Server } from 'http'; From f73041484c9e29ddc06c892b878fee9b93eba9b8 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Mon, 8 Jan 2024 19:58:11 +0000 Subject: [PATCH 07/58] Add spacing to navigation --- packages/toolpad-app/src/toolpad/Toolpad.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/toolpad-app/src/toolpad/Toolpad.tsx b/packages/toolpad-app/src/toolpad/Toolpad.tsx index 159aa33765c..9f4200becf1 100644 --- a/packages/toolpad-app/src/toolpad/Toolpad.tsx +++ b/packages/toolpad-app/src/toolpad/Toolpad.tsx @@ -105,7 +105,9 @@ function EditorShell({ children }: EditorShellProps) { Authorization + + + ) : null } actions={ From ebce000769893eb28c2454e2ebb179be40a2c288 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:58:26 +0000 Subject: [PATCH 08/58] Azure AD auth provider (without role mapping) --- packages/toolpad-app/src/appDom/index.ts | 3 + .../src/components/icons/AzureIcon.tsx | 23 ++++ packages/toolpad-app/src/constants.ts | 2 +- .../toolpad-app/src/runtime/AppLayout.tsx | 9 +- .../toolpad-app/src/runtime/SignInPage.tsx | 119 +++++++++++------- .../toolpad-app/src/runtime/ToolpadApp.tsx | 18 ++- packages/toolpad-app/src/runtime/auth.tsx | 17 ++- packages/toolpad-app/src/runtime/useAuth.ts | 2 +- packages/toolpad-app/src/server/auth.ts | 38 +++++- packages/toolpad-app/src/server/schema.ts | 2 +- .../AppEditor/AppAuthorizationEditor.tsx | 30 +++-- .../AppEditor/PageEditor/PageOptionsPanel.tsx | 22 ++-- .../PagesExplorer/CreatePageNodeDialog.tsx | 3 + .../toolpad/AppEditor/PagesExplorer/index.tsx | 3 + packages/toolpad-app/src/types.ts | 2 +- packages/toolpad-app/typings/@auth.d.ts | 8 ++ 16 files changed, 216 insertions(+), 85 deletions(-) create mode 100644 packages/toolpad-app/src/components/icons/AzureIcon.tsx diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index c3d9b0b05e3..f680caeb298 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -1084,6 +1084,9 @@ export function createDefaultDom(): AppDom { attributes: { title: 'Page 1', display: 'shell', + authorization: { + allowAll: true, + }, }, }); diff --git a/packages/toolpad-app/src/components/icons/AzureIcon.tsx b/packages/toolpad-app/src/components/icons/AzureIcon.tsx new file mode 100644 index 00000000000..bd39a325d4a --- /dev/null +++ b/packages/toolpad-app/src/components/icons/AzureIcon.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; + +interface AzureIconProps { + height?: number; + width?: number; + color?: string; +} + +export default function AzureIcon({ height = 18, width = 18, color = '#fff' }: AzureIconProps) { + return ( + + + + ); +} diff --git a/packages/toolpad-app/src/constants.ts b/packages/toolpad-app/src/constants.ts index 9a8e5503061..5d0b6d21566 100644 --- a/packages/toolpad-app/src/constants.ts +++ b/packages/toolpad-app/src/constants.ts @@ -21,4 +21,4 @@ export const VERSION_CHECK_INTERVAL = 1000 * 60 * 10; // TODO: Remove once global functions UI is ready export const FEATURE_FLAG_GLOBAL_FUNCTIONS = false; -export const FEATURE_FLAG_AUTHORIZATION = false; +export const FEATURE_FLAG_AUTHORIZATION = true; diff --git a/packages/toolpad-app/src/runtime/AppLayout.tsx b/packages/toolpad-app/src/runtime/AppLayout.tsx index ea8ecea86ad..ec5226e44b9 100644 --- a/packages/toolpad-app/src/runtime/AppLayout.tsx +++ b/packages/toolpad-app/src/runtime/AppLayout.tsx @@ -42,6 +42,7 @@ interface AppPagesNavigationProps { pages: NavigationEntry[]; clipped?: boolean; search?: string; + basename: string; } function AppPagesNavigation({ @@ -49,6 +50,7 @@ function AppPagesNavigation({ pages, clipped = false, search, + basename, }: AppPagesNavigationProps) { const navListSubheaderId = React.useId(); @@ -56,8 +58,6 @@ function AppPagesNavigation({ const productIcon = theme.palette.mode === 'dark' ? productIconDark : productIconLight; - const initialPageSlug = pages[0].slug; - return ( ) : null} diff --git a/packages/toolpad-app/src/runtime/SignInPage.tsx b/packages/toolpad-app/src/runtime/SignInPage.tsx index e2de26d1819..75e11bc352c 100644 --- a/packages/toolpad-app/src/runtime/SignInPage.tsx +++ b/packages/toolpad-app/src/runtime/SignInPage.tsx @@ -65,51 +65,80 @@ export default function SignInPage() { You must be authenticated to use this app. - {authProviders.includes('github') ? ( - } - loading={isSigningIn && latestSelectedProvider === 'github'} - disabled={isSigningIn} - loadingPosition="start" - size="large" - sx={{ - backgroundColor: '#24292F', - }} - > - Sign in with GitHub - - ) : null} - {authProviders.includes('google') ? ( - - } - loading={isSigningIn && latestSelectedProvider === 'google'} - disabled={isSigningIn} - loadingPosition="start" - size="large" - sx={{ - backgroundColor: '#fff', - color: '#000', - '&:hover': { - color: theme.palette.primary.contrastText, - }, - }} - > - Sign in with Google - - ) : null} + + {authProviders.includes('github') ? ( + } + loading={isSigningIn && latestSelectedProvider === 'github'} + disabled={isSigningIn} + loadingPosition="start" + size="large" + fullWidth + sx={{ + backgroundColor: '#24292F', + }} + > + Sign in with GitHub + + ) : null} + {authProviders.includes('google') ? ( + + } + loading={isSigningIn && latestSelectedProvider === 'google'} + disabled={isSigningIn} + loadingPosition="start" + size="large" + fullWidth + sx={{ + backgroundColor: '#fff', + color: '#000', + '&:hover': { + color: theme.palette.primary.contrastText, + }, + }} + > + Sign in with Google + + ) : null} + {authProviders.includes('azure-ad') ? ( + + } + loading={isSigningIn && latestSelectedProvider === 'azure-ad'} + disabled={isSigningIn} + loadingPosition="start" + size="large" + fullWidth + sx={{ + backgroundColor: '##0072c6', + }} + > + Sign in with Azure AD + + ) : null} + {pageContent} @@ -1564,19 +1565,27 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { const root = appDom.getApp(dom); const { pages = [] } = appDom.getChildNodes(dom, root); - const { hasAuthentication } = React.useContext(AuthContext); + const { session, hasAuthentication } = React.useContext(AuthContext); const pageMatch = useMatch('/pages/:slug'); const activePageSlug = pageMatch?.params.slug; + const authFilteredPages = React.useMemo(() => { + const userRoles = session?.user?.roles ?? []; + return pages.filter((page) => { + const { allowAll = true, allowedRoles = [] } = page.attributes.authorization ?? {}; + return allowAll || userRoles.some((role) => allowedRoles.includes(role)); + }); + }, [pages, session?.user?.roles]); + const navEntries = React.useMemo( () => - pages.map((page) => ({ + authFilteredPages.map((page) => ({ slug: page.name, displayName: appDom.getPageDisplayName(page), hasShell: page?.attributes.display !== 'standalone', })), - [pages], + [authFilteredPages], ); return ( @@ -1586,6 +1595,7 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { hasNavigation={!IS_RENDERED_IN_CANVAS} hasHeader={hasAuthentication && !IS_RENDERED_IN_CANVAS} clipped={SHOW_PREVIEW_HEADER} + basename={basename} > diff --git a/packages/toolpad-app/src/runtime/auth.tsx b/packages/toolpad-app/src/runtime/auth.tsx index 52f2ab6b887..80efccad26f 100644 --- a/packages/toolpad-app/src/runtime/auth.tsx +++ b/packages/toolpad-app/src/runtime/auth.tsx @@ -5,21 +5,23 @@ import { AUTH_SIGNIN_PATH, AuthContext } from './useAuth'; export interface RequireAuthorizationProps { children?: React.ReactNode; - allowedRole?: string | string[]; + allowAll?: boolean; + allowedRoles?: string[]; basename: string; } export function RequireAuthorization({ children, - allowedRole, + allowAll, + allowedRoles, basename, }: RequireAuthorizationProps) { const { session, isSigningIn } = React.useContext(AuthContext); const user = session?.user ?? null; const allowedRolesSet = React.useMemo>( - () => new Set(asArray(allowedRole ?? [])), - [allowedRole], + () => new Set(asArray(allowedRoles ?? [])), + [allowedRoles], ); React.useEffect(() => { @@ -45,11 +47,8 @@ export function RequireAuthorization({ } let reason = null; - if (!user.roles || user.roles.length <= 0) { - reason = 'User has no roles defined.'; - } else if (!user.roles.some((role) => allowedRolesSet.has(role))) { - const rolesList = user?.roles?.map((role) => JSON.stringify(role)).join(', '); - reason = `User with role(s) ${rolesList} is not allowed access to this resource.`; + if (!allowAll && !user.roles.some((role) => allowedRolesSet.has(role))) { + reason = `User does not have the roles to access this page.`; } // @TODO: Once we have roles we can add back this check. diff --git a/packages/toolpad-app/src/runtime/useAuth.ts b/packages/toolpad-app/src/runtime/useAuth.ts index 72454802146..1ac86a57e7f 100644 --- a/packages/toolpad-app/src/runtime/useAuth.ts +++ b/packages/toolpad-app/src/runtime/useAuth.ts @@ -8,7 +8,7 @@ export const AUTH_CSRF_PATH = `${AUTH_API_PATH}/csrf`; export const AUTH_SIGNIN_PATH = `${AUTH_API_PATH}/signin`; export const AUTH_SIGNOUT_PATH = `${AUTH_API_PATH}/signout`; -export type AuthProvider = 'github' | 'google'; +export type AuthProvider = 'github' | 'google' | 'azure-ad'; export interface AuthSession { user: { diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index ba1ca3e1d0b..17cae8722cf 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -2,6 +2,7 @@ import express, { Router } from 'express'; import { Auth } from '@auth/core'; import GithubProvider, { GitHubEmail, GitHubProfile } from '@auth/core/providers/github'; import GoogleProvider from '@auth/core/providers/google'; +import AzureADProvider from '@auth/core/providers/azure-ad'; import { getToken } from '@auth/core/jwt'; import { AuthConfig, TokenSet } from '@auth/core/types'; import { OAuthConfig } from '@auth/core/providers'; @@ -62,6 +63,12 @@ const googleProvider = GoogleProvider({ }, }); +const azureADProvider = AzureADProvider({ + clientId: process.env.TOOLPAD_AZURE_AD_CLIENT_ID, + clientSecret: process.env.TOOLPAD_AZURE_AD_CLIENT_SECRET, + tenantId: process.env.TOOLPAD_AZURE_AD_TENANT_ID, +}); + export function createAuthHandler(project: ToolpadProject): Router { const { base } = project.options; @@ -74,7 +81,7 @@ export function createAuthHandler(project: ToolpadProject): Router { error: `${base}/signin`, // Error code passed in query string as ?error= verifyRequest: base, }, - providers: [githubProvider, googleProvider], + providers: [githubProvider, googleProvider, azureADProvider], secret: process.env.TOOLPAD_AUTH_SECRET, trustHost: true, callbacks: { @@ -108,11 +115,40 @@ export function createAuthHandler(project: ToolpadProject): Router { ); } + if (account?.provider === 'azure-ad') { + return Boolean( + profile?.email && + (restrictedDomains.length === 0 || + restrictedDomains.some( + (restrictedDomain) => profile.email!.endsWith(`@${restrictedDomain}`) ?? false, + )), + ); + } + return true; }, async redirect({ baseUrl }) { return `${baseUrl}${base}`; }, + jwt({ token, account }) { + if (account?.provider === 'azure-ad' && account.id_token) { + const [, payload] = account.id_token.split('.'); + const idToken = JSON.parse(Buffer.from(payload, 'base64').toString('utf8')); + + token.roles = idToken.roles ?? []; + } + return token; + }, + // @TODO: Types for session callback are broken as it says token does not exist but it does + // Github issue: https://github.com/nextauthjs/next-auth/issues/9437 + // @ts-ignore + session({ session, token }) { + if (session.user) { + session.user.roles = token.roles ?? []; + } + + return session; + }, }, }; diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index cd58c14ce91..a74ea0c0157 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -265,7 +265,7 @@ export const applicationSchema = toolpadObjectSchema( .array( z.object({ provider: z - .enum(['github', 'google']) + .enum(['github', 'google', 'azure-ad']) .describe('Unique identifier for this authentication provider.'), }), ) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 5fc4fb06a85..646d695cf0f 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -19,8 +19,10 @@ import { Stack, Tab, TextField, + Theme, Tooltip, Typography, + useTheme, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import AddIcon from '@mui/icons-material/Add'; @@ -41,16 +43,28 @@ import * as appDom from '../../appDom'; import TabPanel from '../../components/TabPanel'; import { AuthProviderConfig, AuthProvider } from '../../types'; import { updateArray } from '../../utils/immutability'; - -const AUTH_PROVIDERS = new Map([ - ['github', { name: 'GitHub', Icon: GitHubIcon }], - ['google', { name: 'Google', Icon: GoogleIcon }], -]); +import AzureIcon from '../../components/icons/AzureIcon'; + +function getAuthProviderOptions(theme: Theme) { + return new Map([ + ['github', { name: 'GitHub', Icon: GitHubIcon }], + ['google', { name: 'Google', Icon: GoogleIcon }], + [ + 'azure-ad', + { + name: 'Azure AD', + Icon: () => , + }, + ], + ]); +} export function AppAuthenticationEditor() { const { dom } = useAppState(); const appState = useAppStateApi(); + const theme = useTheme(); + const handleAuthProvidersChange = React.useCallback( (event: SelectChangeEvent) => { const { @@ -107,6 +121,8 @@ export function AppAuthenticationEditor() { const restrictedDomains = authentication?.restrictedDomains ?? []; + const authProviderOptions = React.useMemo(() => getAuthProviderOptions(theme), [theme]); + return ( @@ -124,11 +140,11 @@ export function AppAuthenticationEditor() { fullWidth renderValue={(selected) => selected - .map((selectedValue) => AUTH_PROVIDERS.get(selectedValue)?.name ?? '') + .map((selectedValue) => authProviderOptions.get(selectedValue)?.name ?? '') .join(', ') } > - {[...AUTH_PROVIDERS].map(([value, { name, Icon }]) => ( + {[...authProviderOptions].map(([value, { name, Icon }]) => ( -1} /> diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageOptionsPanel.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageOptionsPanel.tsx index 6dd1548d081..a76f57e0704 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageOptionsPanel.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/PageOptionsPanel.tsx @@ -63,18 +63,18 @@ export default function PageOptionsPanel() { const handleAllowAllChange = React.useCallback( (event: React.SyntheticEvent, isAllowed: boolean) => { domApi.update((draft) => - appDom.setNodeNamespacedProp( - draft, - page, - 'attributes', - 'authorization', - isAllowed ? undefined : { allowedRoles: [] }, - ), + appDom.setNodeNamespacedProp(draft, page, 'attributes', 'authorization', { + allowAll: isAllowed, + ...(isAllowed ? { allowedRoles: [] } : {}), + }), ); }, [domApi, page], ); + const allowAll = page.attributes.authorization?.allowAll ?? true; + const allowedRoles = page.attributes.authorization?.allowedRoles ?? []; + return ( Page: @@ -123,17 +123,15 @@ export default function PageOptionsPanel() {
Authorization: - } + control={} label="Allow access to all roles" /> ( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/CreatePageNodeDialog.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/CreatePageNodeDialog.tsx index 89db3253ee1..62dddce7e2b 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/CreatePageNodeDialog.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/CreatePageNodeDialog.tsx @@ -60,6 +60,9 @@ export default function CreatePageDialog({ open, onClose, ...props }: CreatePage attributes: { title: name, display: 'shell', + authorization: { + allowAll: true, + }, }, }); const appNode = appDom.getApp(dom); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx index ff42fa79b70..a1bc515ca56 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PagesExplorer/index.tsx @@ -239,6 +239,9 @@ export default function PagesExplorer({ className }: PagesExplorerProps) { attributes: { title: newPageName, display: 'shell', + authorization: { + allowAll: true, + }, }, }); const appNode = appDom.getApp(dom); diff --git a/packages/toolpad-app/src/types.ts b/packages/toolpad-app/src/types.ts index f62796bf1b9..5ea5b2a91cb 100644 --- a/packages/toolpad-app/src/types.ts +++ b/packages/toolpad-app/src/types.ts @@ -213,7 +213,7 @@ export interface ToolpadProjectOptions { export type CodeEditorFileType = 'resource' | 'component'; -export type AuthProvider = 'github' | 'google'; +export type AuthProvider = 'github' | 'google' | 'azure-ad'; export interface AuthProviderConfig { provider: AuthProvider; diff --git a/packages/toolpad-app/typings/@auth.d.ts b/packages/toolpad-app/typings/@auth.d.ts index e28ef3fb444..66dcb23e64e 100644 --- a/packages/toolpad-app/typings/@auth.d.ts +++ b/packages/toolpad-app/typings/@auth.d.ts @@ -1,5 +1,13 @@ export declare module '@auth/core/types' { + interface User { + roles: string[]; + } interface Profile { verifiedEmails?: string[]; } } +export declare module '@auth/core/jwt' { + interface JWT { + roles: string[]; + } +} From 60f031d0e75369ccb7a3680e8df58abd0a29068b Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 12 Jan 2024 14:33:17 +0000 Subject: [PATCH 09/58] Use just size property in icon --- packages/toolpad-app/src/components/icons/AzureIcon.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/toolpad-app/src/components/icons/AzureIcon.tsx b/packages/toolpad-app/src/components/icons/AzureIcon.tsx index bd39a325d4a..6a72be449d7 100644 --- a/packages/toolpad-app/src/components/icons/AzureIcon.tsx +++ b/packages/toolpad-app/src/components/icons/AzureIcon.tsx @@ -1,17 +1,16 @@ import * as React from 'react'; interface AzureIconProps { - height?: number; - width?: number; + size?: number; color?: string; } -export default function AzureIcon({ height = 18, width = 18, color = '#fff' }: AzureIconProps) { +export default function AzureIcon({ size = 18, color = '#fff' }: AzureIconProps) { return ( Date: Tue, 16 Jan 2024 12:28:13 +0000 Subject: [PATCH 10/58] Add role mapping --- docs/schemas/v1/definitions.json | 145 ++++-------- packages/toolpad-app/src/appDom/index.ts | 3 +- .../toolpad-app/src/runtime/SignInPage.tsx | 2 +- packages/toolpad-app/src/server/auth.ts | 29 ++- packages/toolpad-app/src/server/schema.ts | 14 +- .../AppEditor/AppAuthorizationEditor.tsx | 206 +++++++++++++++++- 6 files changed, 282 insertions(+), 117 deletions(-) diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index 4196e62bef1..46068942643 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -27,16 +27,11 @@ "properties": { "provider": { "type": "string", - "enum": [ - "github", - "google" - ], + "enum": ["github", "google", "azure-ad"], "description": "Unique identifier for this authentication provider." } }, - "required": [ - "provider" - ], + "required": ["provider"], "additionalProperties": false }, "description": "Authentication providers to use." @@ -74,14 +69,28 @@ "description": "A description of the role." } }, - "required": [ - "name" - ], + "required": ["name"], "additionalProperties": false } ] }, "description": "Available roles for this application. These can be assigned to users." + }, + "roleMappings": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "propertyNames": { + "enum": ["github", "google", "azure-ad"] + }, + "description": "Role mapping definitions from authentication provider roles to Toolpad roles." } }, "additionalProperties": false, @@ -92,10 +101,7 @@ "description": "Defines the shape of this \"application\" object" } }, - "required": [ - "apiVersion", - "kind" - ], + "required": ["apiVersion", "kind"], "additionalProperties": false }, "Page": { @@ -185,9 +191,7 @@ "description": "The value" } }, - "required": [ - "name" - ], + "required": ["name"], "additionalProperties": false, "description": "A name/value pair." }, @@ -276,11 +280,7 @@ "type": "string" } }, - "required": [ - "kind", - "content", - "contentType" - ], + "required": ["kind", "content", "contentType"], "additionalProperties": false }, { @@ -297,10 +297,7 @@ } } }, - "required": [ - "kind", - "content" - ], + "required": ["kind", "content"], "additionalProperties": false } ], @@ -324,9 +321,7 @@ "const": "raw" } }, - "required": [ - "kind" - ], + "required": ["kind"], "additionalProperties": false, "description": "Don't interpret this body at all." }, @@ -338,9 +333,7 @@ "const": "json" } }, - "required": [ - "kind" - ], + "required": ["kind"], "additionalProperties": false, "description": "Interpret the fetch response as JSON" }, @@ -356,10 +349,7 @@ "description": "First row contains headers" } }, - "required": [ - "kind", - "headers" - ], + "required": ["kind", "headers"], "additionalProperties": false, "description": "Interpret the fetch response as CSV" }, @@ -371,9 +361,7 @@ "const": "xml" } }, - "required": [ - "kind" - ], + "required": ["kind"], "additionalProperties": false, "description": "Interpret the fetch response as XML" } @@ -381,9 +369,7 @@ "description": "How to parse the response." } }, - "required": [ - "kind" - ], + "required": ["kind"], "additionalProperties": false }, { @@ -399,9 +385,7 @@ "description": "The function to be executed on the backend by this query." } }, - "required": [ - "kind" - ], + "required": ["kind"], "additionalProperties": false } ], @@ -424,9 +408,7 @@ "description": "Time to cache before refetching" } }, - "required": [ - "name" - ], + "required": ["name"], "additionalProperties": false }, "description": "Queries that are used by the page. These will load data when the page opens." @@ -480,10 +462,7 @@ "description": "Defines the shape of this \"page\" object" } }, - "required": [ - "apiVersion", - "kind" - ], + "required": ["apiVersion", "kind"], "additionalProperties": false }, "Theme": { @@ -510,10 +489,7 @@ "properties": { "mode": { "type": "string", - "enum": [ - "light", - "dark" - ] + "enum": ["light", "dark"] }, "primary": { "$ref": "#/definitions/SimplePaletteColorOptions" @@ -533,18 +509,11 @@ "description": "Defines the shape of this \"theme\" object" } }, - "required": [ - "apiVersion", - "kind" - ], + "required": ["apiVersion", "kind"], "additionalProperties": false } }, - "required": [ - "Application", - "Page", - "Theme" - ], + "required": ["Application", "Page", "Theme"], "additionalProperties": false, "definitions": { "JsExpressionBinding": { @@ -555,9 +524,7 @@ "description": "The expression to be evaluated." } }, - "required": [ - "$$jsExpression" - ], + "required": ["$$jsExpression"], "additionalProperties": false, "description": "A binding that evaluates an expression and returns the result." }, @@ -569,9 +536,7 @@ "description": "The name of an environment variable." } }, - "required": [ - "$$env" - ], + "required": ["$$env"], "additionalProperties": false, "description": "An environment variable." }, @@ -583,9 +548,7 @@ "description": "The code to be executed." } }, - "required": [ - "$$jsExpressionAction" - ], + "required": ["$$jsExpressionAction"], "additionalProperties": false, "description": "A javascript expression to be executed when this action is triggered." }, @@ -615,16 +578,11 @@ "description": "Parameters to pass when navigating to this page" } }, - "required": [ - "page", - "parameters" - ], + "required": ["page", "parameters"], "additionalProperties": false } }, - "required": [ - "$$navigationAction" - ], + "required": ["$$navigationAction"], "additionalProperties": false, "description": "A navigation from one page to another, optionally passing parameters to the next page." }, @@ -716,10 +674,7 @@ "description": "The properties to configure this instance of the component." } }, - "required": [ - "component", - "name" - ], + "required": ["component", "name"], "additionalProperties": false, "description": "The instance of a component. Used to build user interfaces in pages." }, @@ -734,9 +689,7 @@ "description": "The subtree, that describes the UI to be rendered by the template." } }, - "required": [ - "$$template" - ], + "required": ["$$template"], "additionalProperties": false, "description": "Describes a fragment of Toolpad elements, to be used as a template." }, @@ -752,10 +705,7 @@ "description": "The value" } }, - "required": [ - "name", - "value" - ], + "required": ["name", "value"], "additionalProperties": false, "description": "a name/value pair with a string value." }, @@ -781,10 +731,7 @@ "description": "The value" } }, - "required": [ - "name", - "value" - ], + "required": ["name", "value"], "additionalProperties": false, "description": "A name/value pair where the value is dynamically bindable to strings." }, @@ -804,11 +751,9 @@ "type": "string" } }, - "required": [ - "main" - ], + "required": ["main"], "additionalProperties": false } }, "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file +} diff --git a/packages/toolpad-app/src/appDom/index.ts b/packages/toolpad-app/src/appDom/index.ts index f680caeb298..b70541f55d0 100644 --- a/packages/toolpad-app/src/appDom/index.ts +++ b/packages/toolpad-app/src/appDom/index.ts @@ -13,7 +13,7 @@ import invariant from 'invariant'; import { BoxProps, ThemeOptions as MuiThemeOptions } from '@mui/material'; import { guessTitle, pascalCase, removeDiacritics, uncapitalize } from '@mui/toolpad-utils/strings'; import { mapProperties, mapValues, hasOwnProperty } from '@mui/toolpad-utils/collections'; -import { AuthProviderConfig, ConnectionStatus } from '../types'; +import { AuthProvider, AuthProviderConfig, ConnectionStatus } from '../types'; import { omit, update, updateOrCreate } from '../utils/immutability'; import { ExactEntriesOf, Maybe } from '../utils/types'; import { envBindingSchema } from '../server/schema'; @@ -66,6 +66,7 @@ export interface AppNode extends AppDomNodeBase { readonly name: string; readonly description?: string; }[]; + readonly roleMappings?: Partial>>; }; }; } diff --git a/packages/toolpad-app/src/runtime/SignInPage.tsx b/packages/toolpad-app/src/runtime/SignInPage.tsx index 75e11bc352c..72800a1f643 100644 --- a/packages/toolpad-app/src/runtime/SignInPage.tsx +++ b/packages/toolpad-app/src/runtime/SignInPage.tsx @@ -65,7 +65,7 @@ export default function SignInPage() { You must be authenticated to use this app. - + {authProviders.includes('github') ? ( role.name) ?? []; + const roleMappings = authorization?.roleMappings?.['azure-ad'] ?? {}; + + token.roles = (idToken.roles ?? []).flatMap((providerRole) => + roleNames + .filter((role) => + roleMappings[role] + ? roleMappings[role].includes(providerRole) + : role === providerRole, + ) + // Remove duplicates in case multiple provider roles map to the same role + .filter((value, index, self) => self.indexOf(value) === index), + ); } + return token; }, // @TODO: Types for session callback are broken as it says token does not exist but it does diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index a74ea0c0157..e552521e57b 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -256,6 +256,8 @@ elementSchema = baseElementSchema }) .describe('The instance of a component. Used to build user interfaces in pages.'); +const authProviderSchema = z.enum(['github', 'google', 'azure-ad']); + export const applicationSchema = toolpadObjectSchema( 'application', z.object({ @@ -264,9 +266,9 @@ export const applicationSchema = toolpadObjectSchema( providers: z .array( z.object({ - provider: z - .enum(['github', 'google', 'azure-ad']) - .describe('Unique identifier for this authentication provider.'), + provider: authProviderSchema.describe( + 'Unique identifier for this authentication provider.', + ), }), ) .optional() @@ -292,6 +294,12 @@ export const applicationSchema = toolpadObjectSchema( ) .optional() .describe('Available roles for this application. These can be assigned to users.'), + roleMappings: z + .record(authProviderSchema, z.record(z.array(z.string()))) + .optional() + .describe( + 'Role mapping definitions from authentication provider roles to Toolpad roles.', + ), }) .optional() .describe('Authorization configuration for this application.'), diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 646d695cf0f..0c08c450d44 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -45,15 +45,22 @@ import { AuthProviderConfig, AuthProvider } from '../../types'; import { updateArray } from '../../utils/immutability'; import AzureIcon from '../../components/icons/AzureIcon'; +interface AuthProviderOption { + name: string; + icon: React.ReactNode; + hasRoles: boolean; +} + function getAuthProviderOptions(theme: Theme) { - return new Map([ - ['github', { name: 'GitHub', Icon: GitHubIcon }], - ['google', { name: 'Google', Icon: GoogleIcon }], + return new Map([ + ['github', { name: 'GitHub', icon: , hasRoles: false }], + ['google', { name: 'Google', icon: , hasRoles: false }], [ 'azure-ad', { name: 'Azure AD', - Icon: () => , + icon: , + hasRoles: true, }, ], ]); @@ -144,11 +151,11 @@ export function AppAuthenticationEditor() { .join(', ') } > - {[...authProviderOptions].map(([value, { name, Icon }]) => ( + {[...authProviderOptions].map(([value, { name, icon }]) => ( -1} /> - + {icon} {name} @@ -452,12 +459,172 @@ export function AppRolesEditor({ onRowUpdateError }: { onRowUpdateError: (error:
); } + +interface RoleMapping { + role: string; + providerRoles: string; +} + +interface RoleMappingRow extends RoleMapping { + id: string; +} + +export function AppRoleMappingsEditor({ + roleEnabledAuthProviderOptions, + onRowUpdateError, +}: { + roleEnabledAuthProviderOptions: [string, AuthProviderOption][]; + onRowUpdateError: (error: Error) => void; +}) { + const { dom } = useAppState(); + const appState = useAppStateApi(); + + const [activeAuthProvider, setAuthProvider] = React.useState( + (roleEnabledAuthProviderOptions[0]?.[0] as AuthProvider) ?? null, + ); + + const handleAuthProviderChange = React.useCallback( + (event: React.ChangeEvent) => { + const { value: provider } = event.target; + + setAuthProvider(provider as AuthProvider); + }, + [], + ); + + const updateRoleMapping = React.useCallback( + (role: string, providerRoles: string) => { + if (!activeAuthProvider) { + return; + } + + appState.update((draft) => { + const app = appDom.getApp(draft); + + draft = appDom.setNodeNamespacedProp(draft, app, 'attributes', 'authorization', { + ...app.attributes?.authorization, + roleMappings: { + ...(app.attributes?.authorization?.roleMappings ?? {}), + [activeAuthProvider]: { + ...(app.attributes?.authorization?.roleMappings?.[activeAuthProvider] ?? {}), + [role]: (providerRoles || role).split(',').map((updatedRole) => updatedRole.trim()), + }, + }, + }); + + return draft; + }); + }, + [activeAuthProvider, appState], + ); + + const roleMappingsRows = React.useMemo(() => { + if (!activeAuthProvider) { + return []; + } + + const appNode = appDom.getApp(dom); + const authorization = appNode.attributes.authorization; + const roles = authorization?.roles ?? []; + const roleMappings = activeAuthProvider + ? authorization?.roleMappings?.[activeAuthProvider] + : {}; + + const existingRows = + roles?.map((role) => ({ + id: role.name, + role: role.name, + providerRoles: roleMappings?.[role.name] ? roleMappings?.[role.name].join(', ') : role.name, + })) ?? []; + + return existingRows; + }, [activeAuthProvider, dom]); + + const roleMappingsColumns = React.useMemo(() => { + return [ + { + field: 'role', + headerName: 'Role', + editable: false, + flex: 0.4, + }, + { + field: 'providerRoles', + headerName: 'Provider roles', + editable: true, + flex: 1, + }, + ]; + }, []); + + const [rowModesModel, setRowModesModel] = React.useState({}); + + const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { + setRowModesModel(newRowModesModel); + }; + + const processRowUpdate = (newRow: GridRowModel): RoleMappingRow => { + updateRoleMapping(newRow.id, newRow.providerRoles); + + return { ...newRow, providerRoles: newRow.providerRoles || newRow.role }; + }; + + return ( + + + {roleEnabledAuthProviderOptions.map(([value, { name }]) => ( + + {name} + + ))} + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
{ + if (Object.keys(rowModesModel).length > 0) { + // Avoid the escape key from closing a dialog this grid is rendered in + event.stopPropagation(); + } + }} + > + +
+
+ ); +} + export interface AppAuthorizationDialogProps { open: boolean; onClose: () => void; } export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizationDialogProps) { + const { dom } = useAppState(); + + const theme = useTheme(); + const [activeTab, setActiveTab] = React.useState<'authentication' | 'roles' | 'users'>( 'authentication', ); @@ -476,6 +643,19 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati setErrorSnackbarMessage(''); }, []); + const roleEnabledAuthProviderOptions = React.useMemo(() => { + const appNode = appDom.getApp(dom); + + const authProviders = (appNode.attributes.authentication?.providers ?? []).map( + (providerConfig) => providerConfig.provider, + ); + const authProviderOptions = getAuthProviderOptions(theme); + + return [...authProviderOptions].filter( + ([optionKey, { hasRoles }]) => hasRoles && authProviders.includes(optionKey as AuthProvider), + ); + }, [dom, theme]); + return ( @@ -488,9 +668,12 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati > + {roleEnabledAuthProviderOptions.length > 0 ? ( + + ) : null} - + @@ -501,6 +684,15 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati
+ + + Define mappings from authentication provider roles to Toolpad roles. + + + From 43c9af0810b8f43bc16a7fc73636e794fd53886a Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:32:17 +0000 Subject: [PATCH 11/58] Update schemas --- docs/schemas/v1/definitions.json | 136 ++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 30 deletions(-) diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index 46068942643..3064f62a3e4 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -27,11 +27,17 @@ "properties": { "provider": { "type": "string", - "enum": ["github", "google", "azure-ad"], + "enum": [ + "github", + "google", + "azure-ad" + ], "description": "Unique identifier for this authentication provider." } }, - "required": ["provider"], + "required": [ + "provider" + ], "additionalProperties": false }, "description": "Authentication providers to use." @@ -69,7 +75,9 @@ "description": "A description of the role." } }, - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false } ] @@ -88,7 +96,11 @@ } }, "propertyNames": { - "enum": ["github", "google", "azure-ad"] + "enum": [ + "github", + "google", + "azure-ad" + ] }, "description": "Role mapping definitions from authentication provider roles to Toolpad roles." } @@ -101,7 +113,10 @@ "description": "Defines the shape of this \"application\" object" } }, - "required": ["apiVersion", "kind"], + "required": [ + "apiVersion", + "kind" + ], "additionalProperties": false }, "Page": { @@ -191,7 +206,9 @@ "description": "The value" } }, - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false, "description": "A name/value pair." }, @@ -280,7 +297,11 @@ "type": "string" } }, - "required": ["kind", "content", "contentType"], + "required": [ + "kind", + "content", + "contentType" + ], "additionalProperties": false }, { @@ -297,7 +318,10 @@ } } }, - "required": ["kind", "content"], + "required": [ + "kind", + "content" + ], "additionalProperties": false } ], @@ -321,7 +345,9 @@ "const": "raw" } }, - "required": ["kind"], + "required": [ + "kind" + ], "additionalProperties": false, "description": "Don't interpret this body at all." }, @@ -333,7 +359,9 @@ "const": "json" } }, - "required": ["kind"], + "required": [ + "kind" + ], "additionalProperties": false, "description": "Interpret the fetch response as JSON" }, @@ -349,7 +377,10 @@ "description": "First row contains headers" } }, - "required": ["kind", "headers"], + "required": [ + "kind", + "headers" + ], "additionalProperties": false, "description": "Interpret the fetch response as CSV" }, @@ -361,7 +392,9 @@ "const": "xml" } }, - "required": ["kind"], + "required": [ + "kind" + ], "additionalProperties": false, "description": "Interpret the fetch response as XML" } @@ -369,7 +402,9 @@ "description": "How to parse the response." } }, - "required": ["kind"], + "required": [ + "kind" + ], "additionalProperties": false }, { @@ -385,7 +420,9 @@ "description": "The function to be executed on the backend by this query." } }, - "required": ["kind"], + "required": [ + "kind" + ], "additionalProperties": false } ], @@ -408,7 +445,9 @@ "description": "Time to cache before refetching" } }, - "required": ["name"], + "required": [ + "name" + ], "additionalProperties": false }, "description": "Queries that are used by the page. These will load data when the page opens." @@ -462,7 +501,10 @@ "description": "Defines the shape of this \"page\" object" } }, - "required": ["apiVersion", "kind"], + "required": [ + "apiVersion", + "kind" + ], "additionalProperties": false }, "Theme": { @@ -489,7 +531,10 @@ "properties": { "mode": { "type": "string", - "enum": ["light", "dark"] + "enum": [ + "light", + "dark" + ] }, "primary": { "$ref": "#/definitions/SimplePaletteColorOptions" @@ -509,11 +554,18 @@ "description": "Defines the shape of this \"theme\" object" } }, - "required": ["apiVersion", "kind"], + "required": [ + "apiVersion", + "kind" + ], "additionalProperties": false } }, - "required": ["Application", "Page", "Theme"], + "required": [ + "Application", + "Page", + "Theme" + ], "additionalProperties": false, "definitions": { "JsExpressionBinding": { @@ -524,7 +576,9 @@ "description": "The expression to be evaluated." } }, - "required": ["$$jsExpression"], + "required": [ + "$$jsExpression" + ], "additionalProperties": false, "description": "A binding that evaluates an expression and returns the result." }, @@ -536,7 +590,9 @@ "description": "The name of an environment variable." } }, - "required": ["$$env"], + "required": [ + "$$env" + ], "additionalProperties": false, "description": "An environment variable." }, @@ -548,7 +604,9 @@ "description": "The code to be executed." } }, - "required": ["$$jsExpressionAction"], + "required": [ + "$$jsExpressionAction" + ], "additionalProperties": false, "description": "A javascript expression to be executed when this action is triggered." }, @@ -578,11 +636,16 @@ "description": "Parameters to pass when navigating to this page" } }, - "required": ["page", "parameters"], + "required": [ + "page", + "parameters" + ], "additionalProperties": false } }, - "required": ["$$navigationAction"], + "required": [ + "$$navigationAction" + ], "additionalProperties": false, "description": "A navigation from one page to another, optionally passing parameters to the next page." }, @@ -674,7 +737,10 @@ "description": "The properties to configure this instance of the component." } }, - "required": ["component", "name"], + "required": [ + "component", + "name" + ], "additionalProperties": false, "description": "The instance of a component. Used to build user interfaces in pages." }, @@ -689,7 +755,9 @@ "description": "The subtree, that describes the UI to be rendered by the template." } }, - "required": ["$$template"], + "required": [ + "$$template" + ], "additionalProperties": false, "description": "Describes a fragment of Toolpad elements, to be used as a template." }, @@ -705,7 +773,10 @@ "description": "The value" } }, - "required": ["name", "value"], + "required": [ + "name", + "value" + ], "additionalProperties": false, "description": "a name/value pair with a string value." }, @@ -731,7 +802,10 @@ "description": "The value" } }, - "required": ["name", "value"], + "required": [ + "name", + "value" + ], "additionalProperties": false, "description": "A name/value pair where the value is dynamically bindable to strings." }, @@ -751,9 +825,11 @@ "type": "string" } }, - "required": ["main"], + "required": [ + "main" + ], "additionalProperties": false } }, "$schema": "http://json-schema.org/draft-07/schema#" -} +} \ No newline at end of file From 1c068260403f18d3745ecdce32c550db618c7bd4 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:34:03 +0000 Subject: [PATCH 12/58] Better name --- .../toolpad/AppEditor/AppAuthorizationEditor.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 0c08c450d44..9896682a538 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -470,17 +470,17 @@ interface RoleMappingRow extends RoleMapping { } export function AppRoleMappingsEditor({ - roleEnabledAuthProviderOptions, + roleEnabledActiveAuthProviderOptions, onRowUpdateError, }: { - roleEnabledAuthProviderOptions: [string, AuthProviderOption][]; + roleEnabledActiveAuthProviderOptions: [string, AuthProviderOption][]; onRowUpdateError: (error: Error) => void; }) { const { dom } = useAppState(); const appState = useAppStateApi(); const [activeAuthProvider, setAuthProvider] = React.useState( - (roleEnabledAuthProviderOptions[0]?.[0] as AuthProvider) ?? null, + (roleEnabledActiveAuthProviderOptions[0]?.[0] as AuthProvider) ?? null, ); const handleAuthProviderChange = React.useCallback( @@ -580,7 +580,7 @@ export function AppRoleMappingsEditor({ select sx={{ mt: 2 }} > - {roleEnabledAuthProviderOptions.map(([value, { name }]) => ( + {roleEnabledActiveAuthProviderOptions.map(([value, { name }]) => ( {name} @@ -643,7 +643,7 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati setErrorSnackbarMessage(''); }, []); - const roleEnabledAuthProviderOptions = React.useMemo(() => { + const roleEnabledActiveAuthProviderOptions = React.useMemo(() => { const appNode = appDom.getApp(dom); const authProviders = (appNode.attributes.authentication?.providers ?? []).map( @@ -668,7 +668,7 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati > - {roleEnabledAuthProviderOptions.length > 0 ? ( + {roleEnabledActiveAuthProviderOptions.length > 0 ? ( ) : null} @@ -690,7 +690,7 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati From f6fdf9559a3f016ce383af17c6d55e3c3553c690 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:22:58 +0000 Subject: [PATCH 13/58] Fix Azure icon --- .../src/components/icons/AzureIcon.tsx | 11 ++--- .../AppEditor/AppAuthorizationEditor.tsx | 43 +++++++------------ 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/packages/toolpad-app/src/components/icons/AzureIcon.tsx b/packages/toolpad-app/src/components/icons/AzureIcon.tsx index 6a72be449d7..cc6a290b33c 100644 --- a/packages/toolpad-app/src/components/icons/AzureIcon.tsx +++ b/packages/toolpad-app/src/components/icons/AzureIcon.tsx @@ -5,17 +5,12 @@ interface AzureIconProps { color?: string; } -export default function AzureIcon({ size = 18, color = '#fff' }: AzureIconProps) { +export default function AzureIcon({ size = 18, color = 'currentColor' }: AzureIconProps) { return ( - + ); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 9896682a538..7a8db613e31 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -19,10 +19,8 @@ import { Stack, Tab, TextField, - Theme, Tooltip, Typography, - useTheme, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import AddIcon from '@mui/icons-material/Add'; @@ -51,27 +49,23 @@ interface AuthProviderOption { hasRoles: boolean; } -function getAuthProviderOptions(theme: Theme) { - return new Map([ - ['github', { name: 'GitHub', icon: , hasRoles: false }], - ['google', { name: 'Google', icon: , hasRoles: false }], - [ - 'azure-ad', - { - name: 'Azure AD', - icon: , - hasRoles: true, - }, - ], - ]); -} +const AUTH_PROVIDER_OPTIONS = new Map([ + ['github', { name: 'GitHub', icon: , hasRoles: false }], + ['google', { name: 'Google', icon: , hasRoles: false }], + [ + 'azure-ad', + { + name: 'Azure AD', + icon: , + hasRoles: true, + }, + ], +]); export function AppAuthenticationEditor() { const { dom } = useAppState(); const appState = useAppStateApi(); - const theme = useTheme(); - const handleAuthProvidersChange = React.useCallback( (event: SelectChangeEvent) => { const { @@ -128,8 +122,6 @@ export function AppAuthenticationEditor() { const restrictedDomains = authentication?.restrictedDomains ?? []; - const authProviderOptions = React.useMemo(() => getAuthProviderOptions(theme), [theme]); - return ( @@ -147,11 +139,11 @@ export function AppAuthenticationEditor() { fullWidth renderValue={(selected) => selected - .map((selectedValue) => authProviderOptions.get(selectedValue)?.name ?? '') + .map((selectedValue) => AUTH_PROVIDER_OPTIONS.get(selectedValue)?.name ?? '') .join(', ') } > - {[...authProviderOptions].map(([value, { name, icon }]) => ( + {[...AUTH_PROVIDER_OPTIONS].map(([value, { name, icon }]) => ( -1} /> @@ -623,8 +615,6 @@ export interface AppAuthorizationDialogProps { export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizationDialogProps) { const { dom } = useAppState(); - const theme = useTheme(); - const [activeTab, setActiveTab] = React.useState<'authentication' | 'roles' | 'users'>( 'authentication', ); @@ -649,12 +639,11 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati const authProviders = (appNode.attributes.authentication?.providers ?? []).map( (providerConfig) => providerConfig.provider, ); - const authProviderOptions = getAuthProviderOptions(theme); - return [...authProviderOptions].filter( + return [...AUTH_PROVIDER_OPTIONS].filter( ([optionKey, { hasRoles }]) => hasRoles && authProviders.includes(optionKey as AuthProvider), ); - }, [dom, theme]); + }, [dom]); return ( From 71ea8a72e3b986306ccf239c70bf2091f25b5450 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:05:31 +0000 Subject: [PATCH 14/58] Disable feature flag --- packages/toolpad-app/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolpad-app/src/constants.ts b/packages/toolpad-app/src/constants.ts index 5d0b6d21566..9a8e5503061 100644 --- a/packages/toolpad-app/src/constants.ts +++ b/packages/toolpad-app/src/constants.ts @@ -21,4 +21,4 @@ export const VERSION_CHECK_INTERVAL = 1000 * 60 * 10; // TODO: Remove once global functions UI is ready export const FEATURE_FLAG_GLOBAL_FUNCTIONS = false; -export const FEATURE_FLAG_AUTHORIZATION = true; +export const FEATURE_FLAG_AUTHORIZATION = false; From 9cffb6a49f1e008a9ae20e4f55036ac5d31c3e33 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:09:08 +0000 Subject: [PATCH 15/58] Self-review --- packages/toolpad-app/src/server/toolpadAppServer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index 155099df7b6..7899f67aedc 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -1,4 +1,3 @@ -import 'dotenv/config'; import * as path from 'path'; import * as fs from 'fs/promises'; import { Server } from 'http'; From f550a027bf57646a4e80acba2541fa327c5b0b46 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:50:28 +0000 Subject: [PATCH 16/58] Fix page blocking logic and default page --- packages/toolpad-app/src/runtime/ToolpadApp.tsx | 17 +++++++++++++---- packages/toolpad-app/src/runtime/auth.tsx | 6 ++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index 1414e7f3480..f6536ce9df4 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -1477,15 +1477,19 @@ function PageNotFound() { interface RenderedPagesProps { pages: appDom.PageNode[]; + defaultPage: appDom.PageNode; hasAuthentication?: boolean; basename: string; } -function RenderedPages({ pages, hasAuthentication = false, basename }: RenderedPagesProps) { +function RenderedPages({ + pages, + defaultPage, + hasAuthentication = false, + basename, +}: RenderedPagesProps) { const { search } = useLocation(); - const defaultPage = pages[0]; - const defaultPageNavigation = ; return ( @@ -1596,7 +1600,12 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { clipped={SHOW_PREVIEW_HEADER} basename={basename} > - + ); } diff --git a/packages/toolpad-app/src/runtime/auth.tsx b/packages/toolpad-app/src/runtime/auth.tsx index 80efccad26f..ca945d8611e 100644 --- a/packages/toolpad-app/src/runtime/auth.tsx +++ b/packages/toolpad-app/src/runtime/auth.tsx @@ -51,16 +51,14 @@ export function RequireAuthorization({ reason = `User does not have the roles to access this page.`; } - // @TODO: Once we have roles we can add back this check. - const skipReason = true; - return !skipReason && reason ? ( + return reason ? ( Unauthorized. {reason} From f725a2129c9421b096f265e6f76c618b5e20d6ed Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 16 Jan 2024 18:32:18 +0000 Subject: [PATCH 17/58] More fixes --- packages/toolpad-app/src/runtime/ToolpadApp.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index f6536ce9df4..8b2bf833164 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -1568,7 +1568,7 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { const root = appDom.getApp(dom); const { pages = [] } = appDom.getChildNodes(dom, root); - const { session, hasAuthentication } = React.useContext(AuthContext); + const { session, hasAuthentication, isSigningIn } = React.useContext(AuthContext); const pageMatch = useMatch('/pages/:slug'); const activePageSlug = pageMatch?.params.slug; @@ -1591,6 +1591,10 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { [authFilteredPages], ); + if (!IS_RENDERED_IN_CANVAS && isSigningIn && hasAuthentication) { + return ; + } + return ( Date: Tue, 16 Jan 2024 18:56:05 +0000 Subject: [PATCH 18/58] Better signout experience --- .../toolpad-app/src/runtime/ToolpadApp.tsx | 14 ++------ packages/toolpad-app/src/runtime/auth.tsx | 32 +++---------------- packages/toolpad-app/src/runtime/useAuth.ts | 10 +++--- 3 files changed, 13 insertions(+), 43 deletions(-) diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index 8b2bf833164..c618bc5be1e 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -1479,15 +1479,9 @@ interface RenderedPagesProps { pages: appDom.PageNode[]; defaultPage: appDom.PageNode; hasAuthentication?: boolean; - basename: string; } -function RenderedPages({ - pages, - defaultPage, - hasAuthentication = false, - basename, -}: RenderedPagesProps) { +function RenderedPages({ pages, defaultPage, hasAuthentication = false }: RenderedPagesProps) { const { search } = useLocation(); const defaultPageNavigation = ; @@ -1509,7 +1503,6 @@ function RenderedPages({ {pageContent} @@ -1568,7 +1561,7 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { const root = appDom.getApp(dom); const { pages = [] } = appDom.getChildNodes(dom, root); - const { session, hasAuthentication, isSigningIn } = React.useContext(AuthContext); + const { session, hasAuthentication } = React.useContext(AuthContext); const pageMatch = useMatch('/pages/:slug'); const activePageSlug = pageMatch?.params.slug; @@ -1591,7 +1584,7 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { [authFilteredPages], ); - if (!IS_RENDERED_IN_CANVAS && isSigningIn && hasAuthentication) { + if (!IS_RENDERED_IN_CANVAS && !session?.user && hasAuthentication) { return ; } @@ -1608,7 +1601,6 @@ function ToolpadAppLayout({ dom, basename }: ToolpadAppLayoutProps) { pages={pages} defaultPage={authFilteredPages[0] ?? pages[0]} hasAuthentication={hasAuthentication} - basename={basename} /> ); diff --git a/packages/toolpad-app/src/runtime/auth.tsx b/packages/toolpad-app/src/runtime/auth.tsx index ca945d8611e..73b85e6dc57 100644 --- a/packages/toolpad-app/src/runtime/auth.tsx +++ b/packages/toolpad-app/src/runtime/auth.tsx @@ -1,22 +1,20 @@ import * as React from 'react'; import { asArray } from '@mui/toolpad-utils/collections'; -import { Box, CircularProgress, Container } from '@mui/material'; -import { AUTH_SIGNIN_PATH, AuthContext } from './useAuth'; +import { Box } from '@mui/material'; +import { AuthContext } from './useAuth'; export interface RequireAuthorizationProps { children?: React.ReactNode; allowAll?: boolean; allowedRoles?: string[]; - basename: string; } export function RequireAuthorization({ children, allowAll, allowedRoles, - basename, }: RequireAuthorizationProps) { - const { session, isSigningIn } = React.useContext(AuthContext); + const { session } = React.useContext(AuthContext); const user = session?.user ?? null; const allowedRolesSet = React.useMemo>( @@ -24,30 +22,8 @@ export function RequireAuthorization({ [allowedRoles], ); - React.useEffect(() => { - if (!user && !isSigningIn) { - window.location.replace(`${basename}${AUTH_SIGNIN_PATH}`); - } - }, [basename, isSigningIn, user]); - - if (!user) { - return ( - - - - ); - } - let reason = null; - if (!allowAll && !user.roles.some((role) => allowedRolesSet.has(role))) { + if (!allowAll && !user?.roles.some((role) => allowedRolesSet.has(role))) { reason = `User does not have the roles to access this page.`; } diff --git a/packages/toolpad-app/src/runtime/useAuth.ts b/packages/toolpad-app/src/runtime/useAuth.ts index 6564f94734b..94bd41c819c 100644 --- a/packages/toolpad-app/src/runtime/useAuth.ts +++ b/packages/toolpad-app/src/runtime/useAuth.ts @@ -3,10 +3,10 @@ import * as appDom from '../appDom'; const AUTH_API_PATH = '/api/auth'; -export const AUTH_SESSION_PATH = `${AUTH_API_PATH}/session`; -export const AUTH_CSRF_PATH = `${AUTH_API_PATH}/csrf`; -export const AUTH_SIGNIN_PATH = `${AUTH_API_PATH}/signin`; -export const AUTH_SIGNOUT_PATH = `${AUTH_API_PATH}/signout`; +const AUTH_SESSION_PATH = `${AUTH_API_PATH}/session`; +const AUTH_CSRF_PATH = `${AUTH_API_PATH}/csrf`; +const AUTH_SIGNIN_PATH = `${AUTH_API_PATH}/signin`; +const AUTH_SIGNOUT_PATH = `${AUTH_API_PATH}/signout`; export type AuthProvider = 'github' | 'google' | 'azure-ad'; @@ -93,6 +93,8 @@ export function useAuth({ dom, basename }: UseAuthInput): AuthPayload { setSession(null); setIsSigningOut(false); + + window.location.replace(`${basename}${AUTH_SIGNIN_PATH}`); }, [basename, getCsrfToken]); const getSession = React.useCallback(async () => { From af6835afb70e334a03ea4b6e458c71e956cc3162 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:43:50 +0000 Subject: [PATCH 19/58] [WIP] Authentication tests --- packages/toolpad-app/src/constants.ts | 2 +- packages/toolpad-app/src/server/auth.ts | 8 +++ .../toolpad-app/src/server/httpApiAdapters.ts | 2 +- packages/toolpad-app/src/server/index.ts | 13 +++-- .../src/server/toolpadAppServer.ts | 14 ++++-- .../auth/fixture/toolpad/.gitignore | 1 + .../auth/fixture/toolpad/application.yml | 7 +++ .../fixture/toolpad/pages/mypage/page.yml | 13 +++++ test/integration/auth/prod.spec.ts | 50 +++++++++++++++++++ .../tmp-6n4uK4/fixture/toolpad/.gitignore | 1 + .../fixture/toolpad/application.yml | 7 +++ .../fixture/toolpad/pages/mypage/page.yml | 13 +++++ test/visual/components/fixture/.gitignore | 1 + .../components/fixture/pages/page/page.yml | 7 +++ 14 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 test/integration/auth/fixture/toolpad/.gitignore create mode 100644 test/integration/auth/fixture/toolpad/application.yml create mode 100644 test/integration/auth/fixture/toolpad/pages/mypage/page.yml create mode 100644 test/integration/auth/prod.spec.ts create mode 100644 test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore create mode 100644 test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml create mode 100644 test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml create mode 100644 test/visual/components/fixture/.gitignore create mode 100644 test/visual/components/fixture/pages/page/page.yml diff --git a/packages/toolpad-app/src/constants.ts b/packages/toolpad-app/src/constants.ts index 9a8e5503061..5d0b6d21566 100644 --- a/packages/toolpad-app/src/constants.ts +++ b/packages/toolpad-app/src/constants.ts @@ -21,4 +21,4 @@ export const VERSION_CHECK_INTERVAL = 1000 * 60 * 10; // TODO: Remove once global functions UI is ready export const FEATURE_FLAG_GLOBAL_FUNCTIONS = false; -export const FEATURE_FLAG_AUTHORIZATION = false; +export const FEATURE_FLAG_AUTHORIZATION = true; diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 2506bbd8f9e..1b7dbdd80a2 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -17,6 +17,9 @@ const SKIP_VERIFICATION_PROVIDERS: AuthProvider[] = [ 'azure-ad', ]; +export const MISSING_SECRET_ERROR_MESSAGE = + 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at [insert link to docs here]'; + export function createAuthHandler(project: ToolpadProject): Router { const { base } = project.options; @@ -176,6 +179,11 @@ export function createAuthHandler(project: ToolpadProject): Router { router.use( '/*', asyncHandler(async (req, res) => { + if (!process.env.TOOLPAD_AUTH_SECRET) { + res.status(400).json({ message: MISSING_SECRET_ERROR_MESSAGE, code: 'MissingSecret' }); + return; + } + const request = adaptRequestFromExpressToFetch(req); const response = (await Auth(request, authConfig)) as Response; diff --git a/packages/toolpad-app/src/server/httpApiAdapters.ts b/packages/toolpad-app/src/server/httpApiAdapters.ts index e0eaf9d0d9e..84007964088 100644 --- a/packages/toolpad-app/src/server/httpApiAdapters.ts +++ b/packages/toolpad-app/src/server/httpApiAdapters.ts @@ -3,7 +3,7 @@ import express from 'express'; export function encodeRequestBody(req: express.Request) { const contentType = req.headers['content-type']; - if (contentType?.includes('application/x-www-form-urlencoded')) { + if (typeof req.body === 'object' && contentType?.includes('application/x-www-form-urlencoded')) { return Object.entries(req.body as Record).reduce((acc, [key, value]) => { const encKey = encodeURIComponent(key); const encValue = encodeURIComponent(value); diff --git a/packages/toolpad-app/src/server/index.ts b/packages/toolpad-app/src/server/index.ts index 7b1f377facb..c6c22315f94 100644 --- a/packages/toolpad-app/src/server/index.ts +++ b/packages/toolpad-app/src/server/index.ts @@ -27,7 +27,11 @@ import { createRpcHandler } from './rpc'; import { APP_URL_WINDOW_PROPERTY } from '../constants'; import { createRpcServer as createProjectRpcServer } from './projectRpcServer'; import { createRpcServer as createRuntimeRpcServer } from './runtimeRpcServer'; -import { createAuthHandler, createRequireAuthMiddleware } from './auth'; +import { + MISSING_SECRET_ERROR_MESSAGE, + createAuthHandler, + createRequireAuthMiddleware, +} from './auth'; import.meta.url ??= url.pathToFileURL(__filename).toString(); const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -114,10 +118,11 @@ async function createDevHandler(project: ToolpadProject) { }), ); - if (process.env.TOOLPAD_AUTH_SECRET) { - const authHandler = createAuthHandler(project); - handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); + if (!process.env.TOOLPAD_AUTH_SECRET) { + console.error(MISSING_SECRET_ERROR_MESSAGE); } + const authHandler = createAuthHandler(project); + handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); handler.use(await createRequireAuthMiddleware(project)); diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index 7899f67aedc..8cb771ef3ff 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -12,7 +12,11 @@ import { RUNTIME_CONFIG_WINDOW_PROPERTY, INITIAL_STATE_WINDOW_PROPERTY } from '. import createRuntimeState from '../runtime/createRuntimeState'; import type { RuntimeConfig } from '../types'; import type { RuntimeState } from '../runtime'; -import { createAuthHandler, createRequireAuthMiddleware } from './auth'; +import { + MISSING_SECRET_ERROR_MESSAGE, + createAuthHandler, + createRequireAuthMiddleware, +} from './auth'; export interface PostProcessHtmlParams { config: RuntimeConfig; @@ -63,11 +67,11 @@ export async function createProdHandler(project: ToolpadProject) { basicAuthUnauthorized(res); }); - if (process.env.TOOLPAD_AUTH_SECRET) { - const authHandler = createAuthHandler(project); - handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); + if (!process.env.TOOLPAD_AUTH_SECRET) { + console.error(MISSING_SECRET_ERROR_MESSAGE); } - + const authHandler = createAuthHandler(project); + handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); handler.use(await createRequireAuthMiddleware(project)); handler.use('/api/data', project.dataManager.createDataHandler()); diff --git a/test/integration/auth/fixture/toolpad/.gitignore b/test/integration/auth/fixture/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/integration/auth/fixture/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/integration/auth/fixture/toolpad/application.yml b/test/integration/auth/fixture/toolpad/application.yml new file mode 100644 index 00000000000..3b44aeea1d8 --- /dev/null +++ b/test/integration/auth/fixture/toolpad/application.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: google }, { provider: github }] + requiredEmail: + - mui.com diff --git a/test/integration/auth/fixture/toolpad/pages/mypage/page.yml b/test/integration/auth/fixture/toolpad/pages/mypage/page.yml new file mode 100644 index 00000000000..a3fdfc98075 --- /dev/null +++ b/test/integration/auth/fixture/toolpad/pages/mypage/page.yml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + displayName: My Page + title: mypage + display: shell + content: + - component: Text + name: text + props: + value: Hello world diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/prod.spec.ts new file mode 100644 index 00000000000..6b49596cba3 --- /dev/null +++ b/test/integration/auth/prod.spec.ts @@ -0,0 +1,50 @@ +import * as path from 'path'; +import * as url from 'url'; +import { test, expect } from '../../playwright/localTest'; + +const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); + +test.use({ + ignoreConsoleErrors: [ + /Failed to load resource: the server responded with a status of 401 \(Unauthorized\)/, + ], +}); + +test.use({ + projectConfig: { + template: path.resolve(currentDirectory, './fixture'), + }, + localAppConfig: { + cmd: 'start', + env: { + TOOLPAD_AUTH_SECRET: 'donttellanyone', + }, + }, +}); + +test('Must redirect to sign-in page if user is not authenticated', async ({ page }) => { + await page.goto('/prod/pages/mypage'); + await expect(page).toHaveURL('/prod/signin'); +}); + +test.only('Must be redirected on sign in', async ({ page }) => { + await page.goto('/prod/signin'); + + const githubLoginButton = page.getByText('Sign in with GitHub'); + + await page.route('*/**/api/auth/csrf', async (route) => { + const json = { csrfToken: 'idontlikehackers' }; + await route.fulfill({ json }); + }); + + await page.route('*/**/api/auth/signin/github', async (route) => { + const json = { url: { signInUrl: '/prod/pages/mypage' } }; + await route.fulfill({ json }); + }); + + await githubLoginButton.click(); + + await expect(page).toHaveURL('/prod/pages/mypage'); +}); + +test('Must be able to view pages if authenticated', async ({ page }) => {}); diff --git a/test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore b/test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml b/test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml new file mode 100644 index 00000000000..3b44aeea1d8 --- /dev/null +++ b/test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: google }, { provider: github }] + requiredEmail: + - mui.com diff --git a/test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml b/test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml new file mode 100644 index 00000000000..a3fdfc98075 --- /dev/null +++ b/test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + displayName: My Page + title: mypage + display: shell + content: + - component: Text + name: text + props: + value: Hello world diff --git a/test/visual/components/fixture/.gitignore b/test/visual/components/fixture/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/visual/components/fixture/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/visual/components/fixture/pages/page/page.yml b/test/visual/components/fixture/pages/page/page.yml new file mode 100644 index 00000000000..c0e71daee55 --- /dev/null +++ b/test/visual/components/fixture/pages/page/page.yml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + id: PLaCtFN + title: Default page From 8bcf78b750b4cc2354fd11bb5e77262017fd51c3 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 5 Jan 2024 15:34:58 +0000 Subject: [PATCH 20/58] Simplify error mesage logic --- packages/toolpad-app/src/server/auth.ts | 6 +++++- packages/toolpad-app/src/server/index.ts | 9 +-------- packages/toolpad-app/src/server/toolpadAppServer.ts | 10 ++-------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 1b7dbdd80a2..612f6b20a8c 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -17,10 +17,14 @@ const SKIP_VERIFICATION_PROVIDERS: AuthProvider[] = [ 'azure-ad', ]; -export const MISSING_SECRET_ERROR_MESSAGE = +const MISSING_SECRET_ERROR_MESSAGE = 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at [insert link to docs here]'; export function createAuthHandler(project: ToolpadProject): Router { + if (!process.env.TOOLPAD_AUTH_SECRET) { + console.error(MISSING_SECRET_ERROR_MESSAGE); + } + const { base } = project.options; const router = express.Router(); diff --git a/packages/toolpad-app/src/server/index.ts b/packages/toolpad-app/src/server/index.ts index c6c22315f94..3f4a4b70807 100644 --- a/packages/toolpad-app/src/server/index.ts +++ b/packages/toolpad-app/src/server/index.ts @@ -27,11 +27,7 @@ import { createRpcHandler } from './rpc'; import { APP_URL_WINDOW_PROPERTY } from '../constants'; import { createRpcServer as createProjectRpcServer } from './projectRpcServer'; import { createRpcServer as createRuntimeRpcServer } from './runtimeRpcServer'; -import { - MISSING_SECRET_ERROR_MESSAGE, - createAuthHandler, - createRequireAuthMiddleware, -} from './auth'; +import { createAuthHandler, createRequireAuthMiddleware } from './auth'; import.meta.url ??= url.pathToFileURL(__filename).toString(); const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -118,9 +114,6 @@ async function createDevHandler(project: ToolpadProject) { }), ); - if (!process.env.TOOLPAD_AUTH_SECRET) { - console.error(MISSING_SECRET_ERROR_MESSAGE); - } const authHandler = createAuthHandler(project); handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index 8cb771ef3ff..37c2410c20b 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -12,11 +12,7 @@ import { RUNTIME_CONFIG_WINDOW_PROPERTY, INITIAL_STATE_WINDOW_PROPERTY } from '. import createRuntimeState from '../runtime/createRuntimeState'; import type { RuntimeConfig } from '../types'; import type { RuntimeState } from '../runtime'; -import { - MISSING_SECRET_ERROR_MESSAGE, - createAuthHandler, - createRequireAuthMiddleware, -} from './auth'; +import { createAuthHandler, createRequireAuthMiddleware } from './auth'; export interface PostProcessHtmlParams { config: RuntimeConfig; @@ -67,11 +63,9 @@ export async function createProdHandler(project: ToolpadProject) { basicAuthUnauthorized(res); }); - if (!process.env.TOOLPAD_AUTH_SECRET) { - console.error(MISSING_SECRET_ERROR_MESSAGE); - } const authHandler = createAuthHandler(project); handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); + handler.use(await createRequireAuthMiddleware(project)); handler.use('/api/data', project.dataManager.createDataHandler()); From 4d8fc43f02ce2a0c994610fae35d5b9e3aaf1bbf Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 5 Jan 2024 19:02:49 +0000 Subject: [PATCH 21/58] Auth test without restricted domains --- test/integration/auth/prod.spec.ts | 72 ++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/prod.spec.ts index 6b49596cba3..2ab266b480a 100644 --- a/test/integration/auth/prod.spec.ts +++ b/test/integration/auth/prod.spec.ts @@ -1,9 +1,19 @@ import * as path from 'path'; import * as url from 'url'; +import { encode } from '@auth/core/jwt'; import { test, expect } from '../../playwright/localTest'; const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); +const TOOLPAD_AUTH_SECRET = 'donttellanyone'; + +const SESSION_USER = { + name: 'Adelbert Steiner', + email: 'steiner@plutoknights.com', + image: 'https://placehold.co/600x400', + roles: [], +}; + test.use({ ignoreConsoleErrors: [ /Failed to load resource: the server responded with a status of 401 \(Unauthorized\)/, @@ -17,34 +27,68 @@ test.use({ localAppConfig: { cmd: 'start', env: { - TOOLPAD_AUTH_SECRET: 'donttellanyone', + TOOLPAD_AUTH_SECRET, }, }, }); -test('Must redirect to sign-in page if user is not authenticated', async ({ page }) => { - await page.goto('/prod/pages/mypage'); - await expect(page).toHaveURL('/prod/signin'); -}); - -test.only('Must be redirected on sign in', async ({ page }) => { - await page.goto('/prod/signin'); +test('Must be authenticated to view pages', async ({ page, context, baseURL }) => { + await page.route('*/**/api/auth/csrf', async (route) => { + const csrfToken = 'idontlikehackers'; - const githubLoginButton = page.getByText('Sign in with GitHub'); + context.addCookies([{ name: 'authjs.csrf-token', value: csrfToken, url: baseURL }]); - await page.route('*/**/api/auth/csrf', async (route) => { - const json = { csrfToken: 'idontlikehackers' }; + const json = { csrfToken }; await route.fulfill({ json }); }); await page.route('*/**/api/auth/signin/github', async (route) => { - const json = { url: { signInUrl: '/prod/pages/mypage' } }; + const token = await encode({ + token: { + user: SESSION_USER, + }, + secret: TOOLPAD_AUTH_SECRET, + salt: 'authjs.session-token', + }); + + context.addCookies([{ name: 'authjs.session-token', value: token, url: baseURL }]); + + const json = { url: '/prod/pages/mypage' }; + await route.fulfill({ json }); + }); + + await page.route('*/**/api/auth/session', async (route) => { + const json = { + user: SESSION_USER, + }; await route.fulfill({ json }); }); + await page.route('*/**/api/auth/signout', async (route) => { + context.clearCookies(); + await route.fulfill({ json: null }); + }); + + await page.goto('/prod/pages/mypage'); + + // Is redirected when unauthenticated + await expect(page).toHaveURL('/prod/signin'); + + const githubLoginButton = page.getByText('Sign in with GitHub'); + await githubLoginButton.click(); + // Goes to correct redirect URL, and is not redirected when authenticated await expect(page).toHaveURL('/prod/pages/mypage'); -}); -test('Must be able to view pages if authenticated', async ({ page }) => {}); + const profileButtonLocator = page.getByText('Adelbert Steiner'); + + await expect(profileButtonLocator).toBeVisible(); + + // Sign out + + await profileButtonLocator.click(); + await page.getByText('Sign out').click(); + + await expect(page).toHaveURL(/\/prod\/signin/); +}); From 11a670432f4b474583664437bc82f1eecb450a57 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 5 Jan 2024 19:10:13 +0000 Subject: [PATCH 22/58] Much more better --- test/integration/auth/prod.spec.ts | 32 +++--------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/prod.spec.ts index 2ab266b480a..96de9838651 100644 --- a/test/integration/auth/prod.spec.ts +++ b/test/integration/auth/prod.spec.ts @@ -7,13 +7,6 @@ const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); const TOOLPAD_AUTH_SECRET = 'donttellanyone'; -const SESSION_USER = { - name: 'Adelbert Steiner', - email: 'steiner@plutoknights.com', - image: 'https://placehold.co/600x400', - roles: [], -}; - test.use({ ignoreConsoleErrors: [ /Failed to load resource: the server responded with a status of 401 \(Unauthorized\)/, @@ -33,19 +26,12 @@ test.use({ }); test('Must be authenticated to view pages', async ({ page, context, baseURL }) => { - await page.route('*/**/api/auth/csrf', async (route) => { - const csrfToken = 'idontlikehackers'; - - context.addCookies([{ name: 'authjs.csrf-token', value: csrfToken, url: baseURL }]); - - const json = { csrfToken }; - await route.fulfill({ json }); - }); - await page.route('*/**/api/auth/signin/github', async (route) => { const token = await encode({ token: { - user: SESSION_USER, + name: 'Adelbert Steiner', + email: 'steiner@plutoknights.com', + picture: 'https://placehold.co/600x400', }, secret: TOOLPAD_AUTH_SECRET, salt: 'authjs.session-token', @@ -57,18 +43,6 @@ test('Must be authenticated to view pages', async ({ page, context, baseURL }) = await route.fulfill({ json }); }); - await page.route('*/**/api/auth/session', async (route) => { - const json = { - user: SESSION_USER, - }; - await route.fulfill({ json }); - }); - - await page.route('*/**/api/auth/signout', async (route) => { - context.clearCookies(); - await route.fulfill({ json: null }); - }); - await page.goto('/prod/pages/mypage'); // Is redirected when unauthenticated From ec4c161ba593be1cdb9bbd0c3f11bfd11ebdbd18 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 5 Jan 2024 19:16:40 +0000 Subject: [PATCH 23/58] Add note to try a better way next --- test/integration/auth/prod.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/prod.spec.ts index 96de9838651..20b9d4ec5ff 100644 --- a/test/integration/auth/prod.spec.ts +++ b/test/integration/auth/prod.spec.ts @@ -26,6 +26,7 @@ test.use({ }); test('Must be authenticated to view pages', async ({ page, context, baseURL }) => { + // @TODO: Try mocking https://api.github.com/user and https://api.github.com/user/emails instead await page.route('*/**/api/auth/signin/github', async (route) => { const token = await encode({ token: { From e1acc0875a7da1eed8a25c2a8e7dd5ec20d3f3e4 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:56:11 +0000 Subject: [PATCH 24/58] Best possible test without creating test users with public credentials --- test/integration/auth/prod.spec.ts | 50 +++++++++--------------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/prod.spec.ts index 20b9d4ec5ff..6b42416e904 100644 --- a/test/integration/auth/prod.spec.ts +++ b/test/integration/auth/prod.spec.ts @@ -26,44 +26,24 @@ test.use({ }); test('Must be authenticated to view pages', async ({ page, context, baseURL }) => { - // @TODO: Try mocking https://api.github.com/user and https://api.github.com/user/emails instead - await page.route('*/**/api/auth/signin/github', async (route) => { - const token = await encode({ - token: { - name: 'Adelbert Steiner', - email: 'steiner@plutoknights.com', - picture: 'https://placehold.co/600x400', - }, - secret: TOOLPAD_AUTH_SECRET, - salt: 'authjs.session-token', - }); - - context.addCookies([{ name: 'authjs.session-token', value: token, url: baseURL }]); - - const json = { url: '/prod/pages/mypage' }; - await route.fulfill({ json }); - }); - - await page.goto('/prod/pages/mypage'); - // Is redirected when unauthenticated + await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL('/prod/signin'); - const githubLoginButton = page.getByText('Sign in with GitHub'); - - await githubLoginButton.click(); + // Authenticate mock user + const token = await encode({ + token: { + name: 'Adelbert Steiner', + email: 'steiner@plutoknights.com', + picture: 'https://placehold.co/600x400', + }, + secret: TOOLPAD_AUTH_SECRET, + salt: 'authjs.session-token', + }); + await context.addCookies([{ name: 'authjs.session-token', value: token, url: baseURL }]); - // Goes to correct redirect URL, and is not redirected when authenticated + // Sees page content when authenticated + await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL('/prod/pages/mypage'); - - const profileButtonLocator = page.getByText('Adelbert Steiner'); - - await expect(profileButtonLocator).toBeVisible(); - - // Sign out - - await profileButtonLocator.click(); - await page.getByText('Sign out').click(); - - await expect(page).toHaveURL(/\/prod\/signin/); + await expect(page.getByText('Adelbert Steiner')).toBeVisible(); }); From 4efa00ff8bc0fc73d869912e54905a38e097f77a Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:59:02 +0000 Subject: [PATCH 25/58] Small refactor --- test/integration/auth/prod.spec.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/prod.spec.ts index 6b42416e904..3832debbdb1 100644 --- a/test/integration/auth/prod.spec.ts +++ b/test/integration/auth/prod.spec.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as url from 'url'; import { encode } from '@auth/core/jwt'; -import { test, expect } from '../../playwright/localTest'; +import { test, expect, BrowserContext } from '../../playwright/localTest'; const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -25,12 +25,10 @@ test.use({ }, }); -test('Must be authenticated to view pages', async ({ page, context, baseURL }) => { - // Is redirected when unauthenticated - await page.goto('/prod/pages/mypage'); - await expect(page).toHaveURL('/prod/signin'); - - // Authenticate mock user +const authenticateUser = async ( + context: BrowserContext, + baseURL: string | undefined, +): Promise => { const token = await encode({ token: { name: 'Adelbert Steiner', @@ -41,9 +39,20 @@ test('Must be authenticated to view pages', async ({ page, context, baseURL }) = salt: 'authjs.session-token', }); await context.addCookies([{ name: 'authjs.session-token', value: token, url: baseURL }]); +}; + +test('Must be authenticated to view pages', async ({ page, context, baseURL }) => { + // Is redirected when unauthenticated + await page.goto('/prod/pages/mypage'); + await expect(page).toHaveURL('/prod/signin'); + + // Authenticate mock user + await authenticateUser(context, baseURL); // Sees page content when authenticated await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL('/prod/pages/mypage'); - await expect(page.getByText('Adelbert Steiner')).toBeVisible(); + + const profileButtonLocator = page.getByText('Adelbert Steiner'); + await expect(profileButtonLocator).toBeVisible(); }); From a1ca40c30edd73e8975969ca3d1c3f99ca30d750 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:00:59 +0000 Subject: [PATCH 26/58] Add credentials provider for testing --- docs/schemas/v1/definitions.json | 6 +- .../toolpad-app/src/runtime/SignInPage.tsx | 278 +++++++++++++----- packages/toolpad-app/src/runtime/useAuth.ts | 29 +- packages/toolpad-app/src/server/auth.ts | 21 +- packages/toolpad-app/src/server/schema.ts | 2 +- .../src/server/toolpadAppBuilder.ts | 2 +- .../AppEditor/AppAuthorizationEditor.tsx | 7 +- packages/toolpad-app/src/types.ts | 2 +- .../auth/fixture/toolpad/application.yml | 2 +- 9 files changed, 249 insertions(+), 100 deletions(-) diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index 3064f62a3e4..10abdb3c4c2 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -30,7 +30,8 @@ "enum": [ "github", "google", - "azure-ad" + "azure-ad", + "credentials" ], "description": "Unique identifier for this authentication provider." } @@ -99,7 +100,8 @@ "enum": [ "github", "google", - "azure-ad" + "azure-ad", + "credentials" ] }, "description": "Role mapping definitions from authentication provider roles to Toolpad roles." diff --git a/packages/toolpad-app/src/runtime/SignInPage.tsx b/packages/toolpad-app/src/runtime/SignInPage.tsx index 72800a1f643..f5f93e6db73 100644 --- a/packages/toolpad-app/src/runtime/SignInPage.tsx +++ b/packages/toolpad-app/src/runtime/SignInPage.tsx @@ -1,14 +1,31 @@ import * as React from 'react'; -import { Alert, Snackbar, Stack, Typography, useTheme } from '@mui/material'; +import { + Alert, + Button, + Divider, + Snackbar, + Stack, + TextField, + Typography, + useTheme, +} from '@mui/material'; import GitHubIcon from '@mui/icons-material/GitHub'; +import PasswordIcon from '@mui/icons-material/Password'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { LoadingButton } from '@mui/lab'; import { useSearchParams } from 'react-router-dom'; +import { useForm, Controller, SubmitHandler } from 'react-hook-form'; import { AuthProvider, AuthContext } from './useAuth'; import productIconDark from '../../public/product-icon-dark.svg'; import productIconLight from '../../public/product-icon-light.svg'; const AUTH_ERROR_URL_PARAM = 'error'; +type CredentialsFormInputs = { + username: string; + password: string; +}; + export default function SignInPage() { const theme = useTheme(); const [urlParams] = useSearchParams(); @@ -20,16 +37,27 @@ export default function SignInPage() { null, ); + const [isCredentialsSignIn, setIsCredentialsSignIn] = React.useState(false); + const { authProviders } = React.useContext(AuthContext); const handleSignIn = React.useCallback( - (provider: AuthProvider) => () => { - setLatestSelectedProvider(provider); - signIn(provider); - }, + (provider: AuthProvider, payload?: Record, isLocalProvider?: boolean) => + () => { + setLatestSelectedProvider(provider); + signIn(provider, payload, isLocalProvider); + }, [signIn], ); + const handleCredentialsSignIn = React.useCallback(() => { + setIsCredentialsSignIn(true); + }, []); + + const handleCredentialsBack = React.useCallback(() => { + setIsCredentialsSignIn(false); + }, []); + React.useEffect(() => { const authError = urlParams.get(AUTH_ERROR_URL_PARAM); @@ -48,6 +76,21 @@ export default function SignInPage() { setErrorSnackbarMessage(''); }, []); + const { handleSubmit: handleCredentialsSubmit, control: credentialsFormControl } = + useForm({ + defaultValues: { + username: '', + password: '', + }, + }); + + const onCredentialsSubmit: SubmitHandler = React.useCallback( + (data) => { + handleSignIn('credentials', data, true)(); + }, + [handleSignIn], + ); + const productIcon = theme.palette.mode === 'dark' ? productIconDark : productIconLight; return ( @@ -66,78 +109,159 @@ export default function SignInPage() { You must be authenticated to use this app. - {authProviders.includes('github') ? ( - } - loading={isSigningIn && latestSelectedProvider === 'github'} - disabled={isSigningIn} - loadingPosition="start" - size="large" - fullWidth - sx={{ - backgroundColor: '#24292F', - }} - > - Sign in with GitHub - - ) : null} - {authProviders.includes('google') ? ( - - } - loading={isSigningIn && latestSelectedProvider === 'google'} - disabled={isSigningIn} - loadingPosition="start" - size="large" - fullWidth - sx={{ - backgroundColor: '#fff', - color: '#000', - '&:hover': { - color: theme.palette.primary.contrastText, - }, - }} - > - Sign in with Google - - ) : null} - {authProviders.includes('azure-ad') ? ( - - } - loading={isSigningIn && latestSelectedProvider === 'azure-ad'} - disabled={isSigningIn} - loadingPosition="start" - size="large" - fullWidth - sx={{ - backgroundColor: '##0072c6', - }} - > - Sign in with Azure AD - - ) : null} + {isCredentialsSignIn ? ( + + + + +
+ + ( + + )} + /> + ( + + )} + /> + + Sign In + + +
+
+ ) : ( + + {authProviders.includes('github') ? ( + } + loading={isSigningIn && latestSelectedProvider === 'github'} + disabled={isSigningIn} + loadingPosition="start" + size="large" + fullWidth + sx={{ + backgroundColor: '#24292F', + }} + > + Sign in with GitHub + + ) : null} + {authProviders.includes('google') ? ( + + } + loading={isSigningIn && latestSelectedProvider === 'google'} + disabled={isSigningIn} + loadingPosition="start" + size="large" + fullWidth + sx={{ + backgroundColor: '#fff', + color: '#000', + '&:hover': { + color: theme.palette.primary.contrastText, + }, + }} + > + Sign in with Google + + ) : null} + {authProviders.includes('azure-ad') ? ( + + } + loading={isSigningIn && latestSelectedProvider === 'azure-ad'} + disabled={isSigningIn} + loadingPosition="start" + size="large" + fullWidth + sx={{ + backgroundColor: '#0072c6', + }} + > + Sign in with Azure AD + + ) : null} + {authProviders.includes('credentials') ? ( + + + OR + + } + loading={isSigningIn && latestSelectedProvider === 'credentials'} + disabled={isSigningIn} + loadingPosition="start" + size="large" + fullWidth + > + Sign in with credentials + + + ) : null} + + )}
void | Promise; + signIn: ( + provider: AuthProvider, + payload?: Record, + isLocalProvider?: boolean, + ) => void | Promise; signOut: () => void | Promise; isSigningIn: boolean; isSigningOut: boolean; @@ -111,7 +115,7 @@ export function useAuth({ dom, basename }: UseAuthInput): AuthPayload { }, [basename, signOut]); const signIn = React.useCallback( - async (provider: AuthProvider) => { + async (provider: AuthProvider, payload?: Record, isLocalProvider = false) => { setIsSigningIn(true); let csrfToken = ''; @@ -122,14 +126,19 @@ export function useAuth({ dom, basename }: UseAuthInput): AuthPayload { } try { - const signInResponse = await fetch(`${basename}${AUTH_SIGNIN_PATH}/${provider}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Auth-Return-Redirect': '1', + const signInResponse = await fetch( + isLocalProvider + ? `${basename}${AUTH_API_PATH}/callback/${provider}` + : `${basename}${AUTH_SIGNIN_PATH}/${provider}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Auth-Return-Redirect': '1', + }, + body: new URLSearchParams({ csrfToken, ...payload }), }, - body: new URLSearchParams({ csrfToken }), - }); + ); const { url: signInUrl } = await signInResponse.json(); window.location.href = signInUrl; diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 612f6b20a8c..4cac639a134 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -3,6 +3,7 @@ import { Auth } from '@auth/core'; import GithubProvider, { GitHubEmail, GitHubProfile } from '@auth/core/providers/github'; import GoogleProvider from '@auth/core/providers/google'; import AzureADProvider from '@auth/core/providers/azure-ad'; +import CredentialsProvider from '@auth/core/providers/credentials'; import { getToken } from '@auth/core/jwt'; import { AuthConfig, TokenSet } from '@auth/core/types'; import { OAuthConfig } from '@auth/core/providers'; @@ -15,6 +16,7 @@ import { AuthProvider } from '../types'; const SKIP_VERIFICATION_PROVIDERS: AuthProvider[] = [ // Azure AD should be fine to skip as the user has to belong to the organization to sign in 'azure-ad', + 'credentials', ]; const MISSING_SECRET_ERROR_MESSAGE = @@ -106,6 +108,17 @@ export function createAuthHandler(project: ToolpadProject): Router { tenantId: process.env.TOOLPAD_AZURE_AD_TENANT_ID, }); + const credentialsProvider = CredentialsProvider({ + name: 'Credentials', + async authorize(credentials) { + if (process.env.NODE_ENV !== 'test') { + throw new Error('Credentials authentication provider can only be used in test mode.'); + } + + return { id: '1', name: 'J Smith', email: 'jsmith@example.com', roles: [] }; + }, + }); + const authConfig: AuthConfig = { pages: { signIn: `${base}/signin`, @@ -113,11 +126,11 @@ export function createAuthHandler(project: ToolpadProject): Router { error: `${base}/signin`, // Error code passed in query string as ?error= verifyRequest: base, }, - providers: [githubProvider, googleProvider, azureADProvider], + providers: [githubProvider, googleProvider, azureADProvider, credentialsProvider], secret: process.env.TOOLPAD_AUTH_SECRET, trustHost: true, callbacks: { - async signIn({ profile, account }) { + async signIn({ profile, account, user }) { const dom = await project.loadDom(); const app = appDom.getApp(dom); @@ -129,10 +142,10 @@ export function createAuthHandler(project: ToolpadProject): Router { return Boolean( (profile?.email_verified || skipEmailVerification) && - profile?.email && + user?.email && (restrictedDomains.length === 0 || restrictedDomains.some( - (restrictedDomain) => profile.email!.endsWith(`@${restrictedDomain}`) ?? false, + (restrictedDomain) => user.email!.endsWith(`@${restrictedDomain}`) ?? false, )), ); }, diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index e552521e57b..164a85df81b 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -256,7 +256,7 @@ elementSchema = baseElementSchema }) .describe('The instance of a component. Used to build user interfaces in pages.'); -const authProviderSchema = z.enum(['github', 'google', 'azure-ad']); +const authProviderSchema = z.enum(['github', 'google', 'azure-ad', 'credentials']); export const applicationSchema = toolpadObjectSchema( 'application', diff --git a/packages/toolpad-app/src/server/toolpadAppBuilder.ts b/packages/toolpad-app/src/server/toolpadAppBuilder.ts index a52f4604f92..fea209ccf82 100644 --- a/packages/toolpad-app/src/server/toolpadAppBuilder.ts +++ b/packages/toolpad-app/src/server/toolpadAppBuilder.ts @@ -312,9 +312,9 @@ if (import.meta.hot) { optimizeDeps: { force: toolpadDevMode ? true : undefined, include: [ - ...FALLBACK_MODULES.map((moduleName) => `@mui/toolpad > ${moduleName}`), '@mui/toolpad/runtime', '@mui/toolpad/canvas', + ...FALLBACK_MODULES.map((moduleName) => `@mui/toolpad > ${moduleName}`), ], }, appType: 'custom', diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 7a8db613e31..87fb83ca008 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -139,6 +139,7 @@ export function AppAuthenticationEditor() { fullWidth renderValue={(selected) => selected + .filter((selectedValue) => AUTH_PROVIDER_OPTIONS.has(selectedValue)) .map((selectedValue) => AUTH_PROVIDER_OPTIONS.get(selectedValue)?.name ?? '') .join(', ') } @@ -636,9 +637,9 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati const roleEnabledActiveAuthProviderOptions = React.useMemo(() => { const appNode = appDom.getApp(dom); - const authProviders = (appNode.attributes.authentication?.providers ?? []).map( - (providerConfig) => providerConfig.provider, - ); + const authProviders = (appNode.attributes.authentication?.providers ?? []) + .filter((providerConfig) => AUTH_PROVIDER_OPTIONS.has(providerConfig.provider)) + .map((providerConfig) => providerConfig.provider); return [...AUTH_PROVIDER_OPTIONS].filter( ([optionKey, { hasRoles }]) => hasRoles && authProviders.includes(optionKey as AuthProvider), diff --git a/packages/toolpad-app/src/types.ts b/packages/toolpad-app/src/types.ts index f856b6891a2..e38d272f939 100644 --- a/packages/toolpad-app/src/types.ts +++ b/packages/toolpad-app/src/types.ts @@ -214,7 +214,7 @@ export interface ToolpadProjectOptions { export type CodeEditorFileType = 'resource' | 'component'; -export type AuthProvider = 'github' | 'google' | 'azure-ad'; +export type AuthProvider = 'github' | 'google' | 'azure-ad' | 'credentials'; export interface AuthProviderConfig { provider: AuthProvider; diff --git a/test/integration/auth/fixture/toolpad/application.yml b/test/integration/auth/fixture/toolpad/application.yml index 3b44aeea1d8..07819369492 100644 --- a/test/integration/auth/fixture/toolpad/application.yml +++ b/test/integration/auth/fixture/toolpad/application.yml @@ -3,5 +3,5 @@ kind: application spec: authentication: providers: [{ provider: google }, { provider: github }] - requiredEmail: + restrictedDomains: - mui.com From 488da7d6170acd5af532ef24961f6db1fea0ffd1 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:20:56 +0000 Subject: [PATCH 27/58] Fix some scenarios with missing secret and unnecessary requests --- .../toolpad-app/src/runtime/SignInPage.tsx | 23 +++--- .../toolpad-app/src/runtime/ToolpadApp.tsx | 2 +- packages/toolpad-app/src/runtime/useAuth.ts | 12 ++-- packages/toolpad-app/src/server/auth.ts | 72 +++++++++++++------ .../src/server/toolpadAppBuilder.ts | 16 ++--- 5 files changed, 79 insertions(+), 46 deletions(-) diff --git a/packages/toolpad-app/src/runtime/SignInPage.tsx b/packages/toolpad-app/src/runtime/SignInPage.tsx index f5f93e6db73..d7a600e6004 100644 --- a/packages/toolpad-app/src/runtime/SignInPage.tsx +++ b/packages/toolpad-app/src/runtime/SignInPage.tsx @@ -26,13 +26,22 @@ type CredentialsFormInputs = { password: string; }; +const azureIconSvg = ( + + + +); + export default function SignInPage() { const theme = useTheme(); const [urlParams] = useSearchParams(); const { signIn, isSigningIn } = React.useContext(AuthContext); - const [errorSnackbarMessage, setErrorSnackbarMessage] = React.useState(''); + const [errorSnackbarMessage, setErrorSnackbarMessage] = React.useState(''); const [latestSelectedProvider, setLatestSelectedProvider] = React.useState( null, ); @@ -67,6 +76,8 @@ export default function SignInPage() { setErrorSnackbarMessage( 'There was an error with your authentication provider configuration.', ); + } else if (authError === 'MissingSecretError') { + setErrorSnackbarMessage('Missing secret for authentication. Please provide a secret.'); } else if (authError) { setErrorSnackbarMessage('An authentication error occurred.'); } @@ -220,15 +231,7 @@ export default function SignInPage() { - } + startIcon={azureIconSvg} loading={isSigningIn && latestSelectedProvider === 'azure-ad'} disabled={isSigningIn} loadingPosition="start" diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index c618bc5be1e..9dcc93d378e 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -1632,7 +1632,7 @@ export default function ToolpadApp({ rootRef, basename, state }: ToolpadAppProps (window as any).toggleDevtools = () => toggleDevtools(); }, [toggleDevtools]); - const authContext = useAuth({ dom, basename }); + const authContext = useAuth({ dom, basename, isRenderedInCanvas: IS_RENDERED_IN_CANVAS }); return ( diff --git a/packages/toolpad-app/src/runtime/useAuth.ts b/packages/toolpad-app/src/runtime/useAuth.ts index 8f759c46032..0850a18bba4 100644 --- a/packages/toolpad-app/src/runtime/useAuth.ts +++ b/packages/toolpad-app/src/runtime/useAuth.ts @@ -46,9 +46,10 @@ export const AuthContext = React.createContext({ interface UseAuthInput { dom: appDom.RenderTree; basename: string; + isRenderedInCanvas?: boolean; } -export function useAuth({ dom, basename }: UseAuthInput): AuthPayload { +export function useAuth({ dom, basename, isRenderedInCanvas = true }: UseAuthInput): AuthPayload { const authProviders = React.useMemo(() => { const app = appDom.getApp(dom); const authProviderConfigs = app.attributes.authentication?.providers ?? []; @@ -139,9 +140,10 @@ export function useAuth({ dom, basename }: UseAuthInput): AuthPayload { body: new URLSearchParams({ csrfToken, ...payload }), }, ); - const { url: signInUrl } = await signInResponse.json(); - window.location.href = signInUrl; + const { url: signInUrl, error } = await signInResponse.json(); + + window.location.href = error ? `${window.location.pathname}?error=${error}` : signInUrl; } catch (error) { console.error((error as Error).message); signOut(); @@ -153,10 +155,10 @@ export function useAuth({ dom, basename }: UseAuthInput): AuthPayload { ); React.useEffect(() => { - if (hasAuthentication) { + if (!isRenderedInCanvas && hasAuthentication) { getSession(); } - }, [getSession, hasAuthentication]); + }, [getSession, hasAuthentication, isRenderedInCanvas]); return { session, diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 4cac639a134..49112dfc965 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -7,6 +7,7 @@ import CredentialsProvider from '@auth/core/providers/credentials'; import { getToken } from '@auth/core/jwt'; import { AuthConfig, TokenSet } from '@auth/core/types'; import { OAuthConfig } from '@auth/core/providers'; +import chalk from 'chalk'; import { asyncHandler } from '../utils/express'; import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; import { ToolpadProject } from './localMode'; @@ -22,9 +23,24 @@ const SKIP_VERIFICATION_PROVIDERS: AuthProvider[] = [ const MISSING_SECRET_ERROR_MESSAGE = 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at [insert link to docs here]'; +function getMappedRoles( + roles: string[], + allRoles: string[], + roleMappings: Record, +): string[] { + return (roles ?? []).flatMap((providerRole) => + allRoles + .filter((role) => + roleMappings[role] ? roleMappings[role].includes(providerRole) : role === providerRole, + ) + // Remove duplicates in case multiple provider roles map to the same role + .filter((value, index, self) => self.indexOf(value) === index), + ); +} + export function createAuthHandler(project: ToolpadProject): Router { if (!process.env.TOOLPAD_AUTH_SECRET) { - console.error(MISSING_SECRET_ERROR_MESSAGE); + console.error(`\n${chalk.red(MISSING_SECRET_ERROR_MESSAGE)}\n`); } const { base } = project.options; @@ -110,12 +126,27 @@ export function createAuthHandler(project: ToolpadProject): Router { const credentialsProvider = CredentialsProvider({ name: 'Credentials', - async authorize(credentials) { + async authorize({ username, password }) { if (process.env.NODE_ENV !== 'test') { throw new Error('Credentials authentication provider can only be used in test mode.'); } - return { id: '1', name: 'J Smith', email: 'jsmith@example.com', roles: [] }; + if (username === 'admin' && password === 'admin') { + return { + id: 'admin', + name: 'Mr. Admin', + email: 'admin@example.com', + roles: ['credentials-admin'], + }; + } + if (username === 'mui' && password === 'mui') { + return { id: 'mui', name: 'MUI', email: 'test@mui.com', roles: [] }; + } + if (username === 'test' && password === 'test') { + return { id: 'test', name: 'Mrs. Test', email: 'test@example.com', roles: [] }; + } + + return null; }, }); @@ -152,30 +183,27 @@ export function createAuthHandler(project: ToolpadProject): Router { async redirect({ baseUrl }) { return `${baseUrl}${base}`; }, - async jwt({ token, account }) { + async jwt({ token, account, user }) { + const dom = await project.loadDom(); + const app = appDom.getApp(dom); + + const authorization = app.attributes.authorization ?? {}; + const roleNames = authorization?.roles?.map((role) => role.name) ?? []; + const roleMappings = account?.provider + ? authorization?.roleMappings?.[account.provider as AuthProvider] ?? {} + : {}; + if (account?.provider === 'azure-ad' && account.id_token) { const [, payload] = account.id_token.split('.'); const idToken: { roles?: string[] } = JSON.parse( Buffer.from(payload, 'base64').toString('utf8'), ); - const dom = await project.loadDom(); - const app = appDom.getApp(dom); - - const authorization = app.attributes.authorization ?? {}; - const roleNames = authorization?.roles?.map((role) => role.name) ?? []; - const roleMappings = authorization?.roleMappings?.['azure-ad'] ?? {}; - - token.roles = (idToken.roles ?? []).flatMap((providerRole) => - roleNames - .filter((role) => - roleMappings[role] - ? roleMappings[role].includes(providerRole) - : role === providerRole, - ) - // Remove duplicates in case multiple provider roles map to the same role - .filter((value, index, self) => self.indexOf(value) === index), - ); + token.roles = getMappedRoles(idToken?.roles ?? [], roleNames, roleMappings); + } + + if (account?.provider === 'credentials') { + token.roles = getMappedRoles(user?.roles ?? [], roleNames, roleMappings); } return token; @@ -197,7 +225,7 @@ export function createAuthHandler(project: ToolpadProject): Router { '/*', asyncHandler(async (req, res) => { if (!process.env.TOOLPAD_AUTH_SECRET) { - res.status(400).json({ message: MISSING_SECRET_ERROR_MESSAGE, code: 'MissingSecret' }); + res.status(400).json({ error: 'MissingSecretError' }); return; } diff --git a/packages/toolpad-app/src/server/toolpadAppBuilder.ts b/packages/toolpad-app/src/server/toolpadAppBuilder.ts index fea209ccf82..407e94304d9 100644 --- a/packages/toolpad-app/src/server/toolpadAppBuilder.ts +++ b/packages/toolpad-app/src/server/toolpadAppBuilder.ts @@ -309,14 +309,14 @@ if (import.meta.hot) { allow: [root, path.resolve(currentDirectory, '../../../../')], }, }, - optimizeDeps: { - force: toolpadDevMode ? true : undefined, - include: [ - '@mui/toolpad/runtime', - '@mui/toolpad/canvas', - ...FALLBACK_MODULES.map((moduleName) => `@mui/toolpad > ${moduleName}`), - ], - }, + // optimizeDeps: { + // force: toolpadDevMode ? true : undefined, + // include: [ + // ...FALLBACK_MODULES.map((moduleName) => `@mui/toolpad > ${moduleName}`), + // '@mui/toolpad/runtime', + // '@mui/toolpad/canvas', + // ], + // }, appType: 'custom', logLevel: 'info', root: currentDirectory, From 68f9de746ad8700d45011f7b498595f333156948 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:59:50 +0000 Subject: [PATCH 28/58] More fixes --- .../toolpad-app/src/runtime/ToolpadApp.tsx | 11 ++----- packages/toolpad-app/src/runtime/useAuth.ts | 4 +-- packages/toolpad-app/src/server/auth.ts | 32 +++++++++---------- packages/toolpad-app/src/server/index.ts | 11 ++++--- .../src/server/toolpadAppServer.ts | 11 ++++--- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index 9dcc93d378e..dd3d2667d14 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -1478,10 +1478,9 @@ function PageNotFound() { interface RenderedPagesProps { pages: appDom.PageNode[]; defaultPage: appDom.PageNode; - hasAuthentication?: boolean; } -function RenderedPages({ pages, defaultPage, hasAuthentication = false }: RenderedPagesProps) { +function RenderedPages({ pages, defaultPage }: RenderedPagesProps) { const { search } = useLocation(); const defaultPageNavigation = ; @@ -1498,7 +1497,7 @@ function RenderedPages({ pages, defaultPage, hasAuthentication = false }: Render /> ); - if (!IS_RENDERED_IN_CANVAS && hasAuthentication) { + if (!IS_RENDERED_IN_CANVAS) { pageContent = ( - + ); } diff --git a/packages/toolpad-app/src/runtime/useAuth.ts b/packages/toolpad-app/src/runtime/useAuth.ts index 0850a18bba4..e9d794f921c 100644 --- a/packages/toolpad-app/src/runtime/useAuth.ts +++ b/packages/toolpad-app/src/runtime/useAuth.ts @@ -141,9 +141,9 @@ export function useAuth({ dom, basename, isRenderedInCanvas = true }: UseAuthInp }, ); - const { url: signInUrl, error } = await signInResponse.json(); + const { url: signInUrl } = await signInResponse.json(); - window.location.href = error ? `${window.location.pathname}?error=${error}` : signInUrl; + window.location.href = signInUrl; } catch (error) { console.error((error as Error).message); signOut(); diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 49112dfc965..e42923c43ed 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -20,8 +20,14 @@ const SKIP_VERIFICATION_PROVIDERS: AuthProvider[] = [ 'credentials', ]; -const MISSING_SECRET_ERROR_MESSAGE = - 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at [insert link to docs here]'; +export async function getHasAuthentication(project: ToolpadProject): Promise { + const dom = await project.loadDom(); + const app = appDom.getApp(dom); + + const authProviders = app.attributes.authentication?.providers ?? []; + + return authProviders.length > 0; +} function getMappedRoles( roles: string[], @@ -40,7 +46,11 @@ function getMappedRoles( export function createAuthHandler(project: ToolpadProject): Router { if (!process.env.TOOLPAD_AUTH_SECRET) { - console.error(`\n${chalk.red(MISSING_SECRET_ERROR_MESSAGE)}\n`); + console.error( + `\n${chalk.red( + 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at [insert link to docs here]', + )}\n`, + ); } const { base } = project.options; @@ -225,7 +235,7 @@ export function createAuthHandler(project: ToolpadProject): Router { '/*', asyncHandler(async (req, res) => { if (!process.env.TOOLPAD_AUTH_SECRET) { - res.status(400).json({ error: 'MissingSecretError' }); + res.status(400).json({ url: `${base}/signin?error=MissingSecretError` }); return; } @@ -254,23 +264,11 @@ export async function createRequireAuthMiddleware(project: ToolpadProject) { const { options } = project; const { base } = options; - const dom = await project.loadDom(); - - const app = appDom.getApp(dom); - - const authProviders = app.attributes.authentication?.providers ?? []; - - const hasAuthentication = authProviders.length > 0; - const isPageRequest = req.get('sec-fetch-dest') === 'document'; const signInPath = `${base}/signin`; let isAuthorized = true; - if ( - (!project.options.dev || isPageRequest) && - hasAuthentication && - req.originalUrl.split('?')[0] !== signInPath - ) { + if ((!project.options.dev || isPageRequest) && req.originalUrl.split('?')[0] !== signInPath) { const request = adaptRequestFromExpressToFetch(req); let token; diff --git a/packages/toolpad-app/src/server/index.ts b/packages/toolpad-app/src/server/index.ts index 3f4a4b70807..83ae1a35e5a 100644 --- a/packages/toolpad-app/src/server/index.ts +++ b/packages/toolpad-app/src/server/index.ts @@ -27,7 +27,7 @@ import { createRpcHandler } from './rpc'; import { APP_URL_WINDOW_PROPERTY } from '../constants'; import { createRpcServer as createProjectRpcServer } from './projectRpcServer'; import { createRpcServer as createRuntimeRpcServer } from './runtimeRpcServer'; -import { createAuthHandler, createRequireAuthMiddleware } from './auth'; +import { createAuthHandler, createRequireAuthMiddleware, getHasAuthentication } from './auth'; import.meta.url ??= url.pathToFileURL(__filename).toString(); const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -114,10 +114,13 @@ async function createDevHandler(project: ToolpadProject) { }), ); - const authHandler = createAuthHandler(project); - handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); + const hasAuthentication = await getHasAuthentication(project); + if (hasAuthentication) { + const authHandler = createAuthHandler(project); + handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); - handler.use(await createRequireAuthMiddleware(project)); + handler.use(await createRequireAuthMiddleware(project)); + } handler.use('/api/data', project.dataManager.createDataHandler()); const runtimeRpcServer = createRuntimeRpcServer(project); diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index 37c2410c20b..3a4f5b5e37e 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -12,7 +12,7 @@ import { RUNTIME_CONFIG_WINDOW_PROPERTY, INITIAL_STATE_WINDOW_PROPERTY } from '. import createRuntimeState from '../runtime/createRuntimeState'; import type { RuntimeConfig } from '../types'; import type { RuntimeState } from '../runtime'; -import { createAuthHandler, createRequireAuthMiddleware } from './auth'; +import { createAuthHandler, createRequireAuthMiddleware, getHasAuthentication } from './auth'; export interface PostProcessHtmlParams { config: RuntimeConfig; @@ -63,10 +63,13 @@ export async function createProdHandler(project: ToolpadProject) { basicAuthUnauthorized(res); }); - const authHandler = createAuthHandler(project); - handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); + const hasAuthentication = await getHasAuthentication(project); + if (hasAuthentication) { + const authHandler = createAuthHandler(project); + handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); - handler.use(await createRequireAuthMiddleware(project)); + handler.use(await createRequireAuthMiddleware(project)); + } handler.use('/api/data', project.dataManager.createDataHandler()); From 9d6b82a25d749388f39585bc3cbbd8ce6eb87e19 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:23:40 +0000 Subject: [PATCH 29/58] Fix CSRF bullshit, add test with authentication, sign in, sign out and restricted domains --- .../toolpad-app/src/runtime/SignInPage.tsx | 10 ++- packages/toolpad-app/src/runtime/useAuth.ts | 47 ++++++------ packages/toolpad-app/src/server/auth.ts | 12 +-- .../AppEditor/AppAuthorizationEditor.tsx | 2 +- pnpm-lock.yaml | 76 ++++++++++++++++++- .../auth/fixture/toolpad/application.yml | 2 +- test/integration/auth/prod.spec.ts | 62 ++++++++------- 7 files changed, 147 insertions(+), 64 deletions(-) diff --git a/packages/toolpad-app/src/runtime/SignInPage.tsx b/packages/toolpad-app/src/runtime/SignInPage.tsx index d7a600e6004..3856b0c13e3 100644 --- a/packages/toolpad-app/src/runtime/SignInPage.tsx +++ b/packages/toolpad-app/src/runtime/SignInPage.tsx @@ -173,7 +173,7 @@ export default function SignInPage() { size="large" fullWidth > - Sign In + Sign in
@@ -246,9 +246,11 @@ export default function SignInPage() { ) : null} {authProviders.includes('credentials') ? ( - - OR - + {authProviders.length > 1 ? ( + + OR + + ) : null} { - const csrfResponse = await fetch(`${basename}${AUTH_CSRF_PATH}`, { - headers: { - 'Content-Type': 'application/json', - }, - }); - const { csrfToken } = await csrfResponse.json(); + let csrfToken = ''; + try { + const csrfResponse = await fetch(`${basename}${AUTH_CSRF_PATH}`, { + headers: { + 'Content-Type': 'application/json', + }, + }); + csrfToken = (await csrfResponse.json())?.csrfToken; + } catch (error) { + console.error((error as Error).message); + } return csrfToken ?? ''; }, [basename]); @@ -76,12 +81,7 @@ export function useAuth({ dom, basename, isRenderedInCanvas = true }: UseAuthInp const signOut = React.useCallback(async () => { setIsSigningOut(true); - let csrfToken = ''; - try { - csrfToken = await getCsrfToken(); - } catch (error) { - console.error((error as Error).message); - } + const csrfToken = await getCsrfToken(); try { await fetch(`${basename}${AUTH_SIGNOUT_PATH}`, { @@ -103,9 +103,17 @@ export function useAuth({ dom, basename, isRenderedInCanvas = true }: UseAuthInp }, [basename, getCsrfToken]); const getSession = React.useCallback(async () => { + setIsSigningIn(true); + + await getCsrfToken(); + try { setIsSigningIn(true); - const sessionResponse = await fetch(`${basename}${AUTH_SESSION_PATH}`); + const sessionResponse = await fetch(`${basename}${AUTH_SESSION_PATH}`, { + headers: { + 'Content-Type': 'application/json', + }, + }); setSession(await sessionResponse.json()); } catch (error) { console.error((error as Error).message); @@ -113,18 +121,13 @@ export function useAuth({ dom, basename, isRenderedInCanvas = true }: UseAuthInp } setIsSigningIn(false); - }, [basename, signOut]); + }, [basename, getCsrfToken, signOut]); const signIn = React.useCallback( async (provider: AuthProvider, payload?: Record, isLocalProvider = false) => { setIsSigningIn(true); - let csrfToken = ''; - try { - csrfToken = await getCsrfToken(); - } catch (error) { - console.error((error as Error).message); - } + const csrfToken = await getCsrfToken(); try { const signInResponse = await fetch( @@ -137,7 +140,7 @@ export function useAuth({ dom, basename, isRenderedInCanvas = true }: UseAuthInp 'Content-Type': 'application/x-www-form-urlencoded', 'X-Auth-Return-Redirect': '1', }, - body: new URLSearchParams({ csrfToken, ...payload }), + body: new URLSearchParams({ ...payload, csrfToken }), }, ); @@ -158,7 +161,7 @@ export function useAuth({ dom, basename, isRenderedInCanvas = true }: UseAuthInp if (!isRenderedInCanvas && hasAuthentication) { getSession(); } - }, [getSession, hasAuthentication, isRenderedInCanvas]); + }, [getCsrfToken, getSession, hasAuthentication, isRenderedInCanvas]); return { session, diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index e42923c43ed..09197e19b3f 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -48,7 +48,7 @@ export function createAuthHandler(project: ToolpadProject): Router { if (!process.env.TOOLPAD_AUTH_SECRET) { console.error( `\n${chalk.red( - 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at [insert link to docs here]', + 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at https://mui.com/toolpad/concepts/authentication/#authentication-providers', )}\n`, ); } @@ -134,7 +134,7 @@ export function createAuthHandler(project: ToolpadProject): Router { tenantId: process.env.TOOLPAD_AZURE_AD_TENANT_ID, }); - const credentialsProvider = CredentialsProvider({ + const mockCredentialsProvider = CredentialsProvider({ name: 'Credentials', async authorize({ username, password }) { if (process.env.NODE_ENV !== 'test') { @@ -144,16 +144,16 @@ export function createAuthHandler(project: ToolpadProject): Router { if (username === 'admin' && password === 'admin') { return { id: 'admin', - name: 'Mr. Admin', + name: 'Lord Admin', email: 'admin@example.com', roles: ['credentials-admin'], }; } if (username === 'mui' && password === 'mui') { - return { id: 'mui', name: 'MUI', email: 'test@mui.com', roles: [] }; + return { id: 'mui', name: 'Mr. MUI 2024', email: 'test@mui.com', roles: [] }; } if (username === 'test' && password === 'test') { - return { id: 'test', name: 'Mrs. Test', email: 'test@example.com', roles: [] }; + return { id: 'test', name: 'Miss Test', email: 'test@example.com', roles: [] }; } return null; @@ -167,7 +167,7 @@ export function createAuthHandler(project: ToolpadProject): Router { error: `${base}/signin`, // Error code passed in query string as ?error= verifyRequest: base, }, - providers: [githubProvider, googleProvider, azureADProvider, credentialsProvider], + providers: [githubProvider, googleProvider, azureADProvider, mockCredentialsProvider], secret: process.env.TOOLPAD_AUTH_SECRET, trustHost: true, callbacks: { diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 87fb83ca008..cf12bf11aa2 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -160,7 +160,7 @@ export function AppAuthenticationEditor() { Certain environment variables must be set for authentication providers to work.{' '} - + Learn how to set up authentication . diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f06bda32e2a..ece2cb6f9f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -825,7 +825,7 @@ importers: version: 9.1.0(eslint@8.56.0) eslint-plugin-import: specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@6.18.1)(eslint-import-resolver-webpack@0.13.8)(eslint@8.56.0) + version: 2.29.1(eslint@8.56.0) formidable: specifier: 3.5.1 version: 3.5.1 @@ -7383,6 +7383,34 @@ packages: transitivePeerDependencies: - supports-color + /eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + debug: 3.2.7 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-filenames@1.3.2(eslint@8.56.0): resolution: {integrity: sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==} peerDependencies: @@ -7429,6 +7457,40 @@ packages: - eslint-import-resolver-webpack - supports-color + /eslint-plugin-import@2.29.1(eslint@8.56.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-jsx-a11y@6.8.0(eslint@8.56.0): resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} engines: {node: '>=4.0'} @@ -7977,6 +8039,16 @@ packages: /flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + /follow-redirects@1.15.4: + resolution: {integrity: sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /follow-redirects@1.15.4(debug@4.3.4): resolution: {integrity: sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==} engines: {node: '>=4.0'} @@ -8807,7 +8879,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.4(debug@4.3.4) + follow-redirects: 1.15.4 requires-port: 1.0.0 transitivePeerDependencies: - debug diff --git a/test/integration/auth/fixture/toolpad/application.yml b/test/integration/auth/fixture/toolpad/application.yml index 07819369492..0a6f88374e7 100644 --- a/test/integration/auth/fixture/toolpad/application.yml +++ b/test/integration/auth/fixture/toolpad/application.yml @@ -2,6 +2,6 @@ apiVersion: v1 kind: application spec: authentication: - providers: [{ provider: google }, { provider: github }] + providers: [{ provider: credentials }] restrictedDomains: - mui.com diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/prod.spec.ts index 3832debbdb1..64cf9ac0326 100644 --- a/test/integration/auth/prod.spec.ts +++ b/test/integration/auth/prod.spec.ts @@ -1,12 +1,9 @@ import * as path from 'path'; import * as url from 'url'; -import { encode } from '@auth/core/jwt'; -import { test, expect, BrowserContext } from '../../playwright/localTest'; +import { test, expect } from '../../playwright/localTest'; const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); -const TOOLPAD_AUTH_SECRET = 'donttellanyone'; - test.use({ ignoreConsoleErrors: [ /Failed to load resource: the server responded with a status of 401 \(Unauthorized\)/, @@ -20,39 +17,48 @@ test.use({ localAppConfig: { cmd: 'start', env: { - TOOLPAD_AUTH_SECRET, + TOOLPAD_AUTH_SECRET: 'donttellanyone', + NODE_ENV: 'test', }, }, }); -const authenticateUser = async ( - context: BrowserContext, - baseURL: string | undefined, -): Promise => { - const token = await encode({ - token: { - name: 'Adelbert Steiner', - email: 'steiner@plutoknights.com', - picture: 'https://placehold.co/600x400', - }, - secret: TOOLPAD_AUTH_SECRET, - salt: 'authjs.session-token', - }); - await context.addCookies([{ name: 'authjs.session-token', value: token, url: baseURL }]); -}; - -test('Must be authenticated to view pages', async ({ page, context, baseURL }) => { +test('Must be authenticated with valid domain to view pages', async ({ page }) => { // Is redirected when unauthenticated await page.goto('/prod/pages/mypage'); - await expect(page).toHaveURL('/prod/signin'); + await expect(page).toHaveURL(/\/prod\/signin/); + + const tryCredentialsSignIn = async (username: string, password: string) => { + // Sign in with invalid domain + await page.getByText('Sign in with credentials').click(); + await page.getByLabel('Username').fill(username); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Sign in' }).click(); + }; + + // Sign in with invalid domain + await tryCredentialsSignIn('test', 'test'); + + await expect(page).toHaveURL(/\/prod\/signin/); + await expect(page.getByText('Access unauthorized.')).toBeVisible(); - // Authenticate mock user - await authenticateUser(context, baseURL); + // Sign in with valid domain + await tryCredentialsSignIn('mui', 'mui'); // Sees page content when authenticated + await expect(page).toHaveURL(/\/prod\/pages\/mypage/); + + // Is not redirected when unauthenticated await page.goto('/prod/pages/mypage'); - await expect(page).toHaveURL('/prod/pages/mypage'); + await expect(page).toHaveURL(/\/prod\/pages\/mypage/); + + // Sign out + await page.getByText('Mr. MUI 2024').click(); + await page.getByText('Sign out').click(); + + await expect(page).toHaveURL(/\/prod\/signin/); - const profileButtonLocator = page.getByText('Adelbert Steiner'); - await expect(profileButtonLocator).toBeVisible(); + // Is redirected when unauthenticated + await page.goto('/prod/pages/mypage'); + await expect(page).toHaveURL(/\/prod\/signin/); }); From 188ef5843577048e93a61341772a5b1294b383fb Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:58:26 +0000 Subject: [PATCH 30/58] Add roles test --- packages/toolpad-app/src/server/auth.ts | 2 +- .../auth/{prod.spec.ts => basic.spec.ts} | 20 +++------ .../toolpad/.gitignore | 0 .../toolpad/application.yml | 0 .../toolpad/pages/mypage/page.yml | 0 .../integration/auth/fixture-roles/.gitignore | 1 + .../auth/fixture-roles/pages/page/page.yml | 7 +++ .../auth/fixture-roles/toolpad/.gitignore | 1 + .../fixture-roles/toolpad/application.yml | 11 +++++ .../toolpad/pages/adminpage/page.yml | 18 ++++++++ .../toolpad/pages/publicpage/page.yml | 15 +++++++ test/integration/auth/roles.spec.ts | 44 +++++++++++++++++++ test/integration/auth/shared.ts | 8 ++++ 13 files changed, 111 insertions(+), 16 deletions(-) rename test/integration/auth/{prod.spec.ts => basic.spec.ts} (67%) rename test/integration/auth/{fixture => fixture-basic}/toolpad/.gitignore (100%) rename test/integration/auth/{fixture => fixture-basic}/toolpad/application.yml (100%) rename test/integration/auth/{fixture => fixture-basic}/toolpad/pages/mypage/page.yml (100%) create mode 100644 test/integration/auth/fixture-roles/.gitignore create mode 100644 test/integration/auth/fixture-roles/pages/page/page.yml create mode 100644 test/integration/auth/fixture-roles/toolpad/.gitignore create mode 100644 test/integration/auth/fixture-roles/toolpad/application.yml create mode 100644 test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml create mode 100644 test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml create mode 100644 test/integration/auth/roles.spec.ts create mode 100644 test/integration/auth/shared.ts diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 09197e19b3f..c41187d1fe2 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -146,7 +146,7 @@ export function createAuthHandler(project: ToolpadProject): Router { id: 'admin', name: 'Lord Admin', email: 'admin@example.com', - roles: ['credentials-admin'], + roles: ['mock-admin'], }; } if (username === 'mui' && password === 'mui') { diff --git a/test/integration/auth/prod.spec.ts b/test/integration/auth/basic.spec.ts similarity index 67% rename from test/integration/auth/prod.spec.ts rename to test/integration/auth/basic.spec.ts index 64cf9ac0326..7e5ee0ac1ca 100644 --- a/test/integration/auth/prod.spec.ts +++ b/test/integration/auth/basic.spec.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import * as url from 'url'; import { test, expect } from '../../playwright/localTest'; +import { tryCredentialsSignIn } from './shared'; const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -12,7 +13,7 @@ test.use({ test.use({ projectConfig: { - template: path.resolve(currentDirectory, './fixture'), + template: path.resolve(currentDirectory, './fixture-basic'), }, localAppConfig: { cmd: 'start', @@ -23,29 +24,18 @@ test.use({ }, }); -test('Must be authenticated with valid domain to view pages', async ({ page }) => { +test('Must be authenticated with valid domain to view app', async ({ page }) => { // Is redirected when unauthenticated await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL(/\/prod\/signin/); - const tryCredentialsSignIn = async (username: string, password: string) => { - // Sign in with invalid domain - await page.getByText('Sign in with credentials').click(); - await page.getByLabel('Username').fill(username); - await page.getByLabel('Password').fill(password); - await page.getByRole('button', { name: 'Sign in' }).click(); - }; - // Sign in with invalid domain - await tryCredentialsSignIn('test', 'test'); - + await tryCredentialsSignIn(page, 'test', 'test'); await expect(page).toHaveURL(/\/prod\/signin/); await expect(page.getByText('Access unauthorized.')).toBeVisible(); // Sign in with valid domain - await tryCredentialsSignIn('mui', 'mui'); - - // Sees page content when authenticated + await tryCredentialsSignIn(page, 'mui', 'mui'); await expect(page).toHaveURL(/\/prod\/pages\/mypage/); // Is not redirected when unauthenticated diff --git a/test/integration/auth/fixture/toolpad/.gitignore b/test/integration/auth/fixture-basic/toolpad/.gitignore similarity index 100% rename from test/integration/auth/fixture/toolpad/.gitignore rename to test/integration/auth/fixture-basic/toolpad/.gitignore diff --git a/test/integration/auth/fixture/toolpad/application.yml b/test/integration/auth/fixture-basic/toolpad/application.yml similarity index 100% rename from test/integration/auth/fixture/toolpad/application.yml rename to test/integration/auth/fixture-basic/toolpad/application.yml diff --git a/test/integration/auth/fixture/toolpad/pages/mypage/page.yml b/test/integration/auth/fixture-basic/toolpad/pages/mypage/page.yml similarity index 100% rename from test/integration/auth/fixture/toolpad/pages/mypage/page.yml rename to test/integration/auth/fixture-basic/toolpad/pages/mypage/page.yml diff --git a/test/integration/auth/fixture-roles/.gitignore b/test/integration/auth/fixture-roles/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/integration/auth/fixture-roles/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/integration/auth/fixture-roles/pages/page/page.yml b/test/integration/auth/fixture-roles/pages/page/page.yml new file mode 100644 index 00000000000..45c4b67e93c --- /dev/null +++ b/test/integration/auth/fixture-roles/pages/page/page.yml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.45/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + id: Qz9ETeM + title: Default page diff --git a/test/integration/auth/fixture-roles/toolpad/.gitignore b/test/integration/auth/fixture-roles/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/integration/auth/fixture-roles/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/integration/auth/fixture-roles/toolpad/application.yml b/test/integration/auth/fixture-roles/toolpad/application.yml new file mode 100644 index 00000000000..c67db9006b5 --- /dev/null +++ b/test/integration/auth/fixture-roles/toolpad/application.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: credentials }] + restrictedDomains: [] + authorization: + { + roles: [{ name: admin, description: A very important person. }], + roleMappings: { credentials: { admin: ['mock-admin', 'god'] } }, + } diff --git a/test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml b/test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml new file mode 100644 index 00000000000..0d770a2bf65 --- /dev/null +++ b/test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.45/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: adminpage + display: shell + authorization: + allowAll: false + allowedRoles: + - admin + displayName: Admin Page + content: + - component: Text + name: text + props: + value: I just want to tell all the admins out there to never stop + administrating. diff --git a/test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml b/test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml new file mode 100644 index 00000000000..e92b9373880 --- /dev/null +++ b/test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.45/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: publicpage + display: shell + authorization: + allowAll: true + displayName: Public Page + content: + - component: Text + name: text + props: + value: This page is open to all, regardless of gender, race or social status. diff --git a/test/integration/auth/roles.spec.ts b/test/integration/auth/roles.spec.ts new file mode 100644 index 00000000000..0974b9bcf7a --- /dev/null +++ b/test/integration/auth/roles.spec.ts @@ -0,0 +1,44 @@ +import * as path from 'path'; +import * as url from 'url'; +import { test, expect } from '../../playwright/localTest'; +import { tryCredentialsSignIn } from './shared'; + +const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); + +test.use({ + ignoreConsoleErrors: [ + /Failed to load resource: the server responded with a status of 401 \(Unauthorized\)/, + ], +}); + +test.use({ + projectConfig: { + template: path.resolve(currentDirectory, './fixture-roles'), + }, + localAppConfig: { + cmd: 'start', + env: { + TOOLPAD_AUTH_SECRET: 'donttellanyone', + NODE_ENV: 'test', + }, + }, +}); + +test.only('Must have required roles to view pages', async ({ page }) => { + await page.goto('/prod/signin'); + + // Sign in without admin role + await tryCredentialsSignIn(page, 'test', 'test'); + + await expect(page.getByText('Admin Page')).toBeHidden(); + + // Sign in with admin role + await page.getByText('Miss Test').click(); + await page.getByText('Sign out').click(); + await tryCredentialsSignIn(page, 'admin', 'admin'); + + await expect(page.getByText('Admin Page')).toBeVisible(); + await expect( + page.getByText('I just want to tell all the admins out there to never stop administrating.'), + ).toBeVisible(); +}); diff --git a/test/integration/auth/shared.ts b/test/integration/auth/shared.ts new file mode 100644 index 00000000000..7640b7eb1c1 --- /dev/null +++ b/test/integration/auth/shared.ts @@ -0,0 +1,8 @@ +import { Page } from '@playwright/test'; + +export async function tryCredentialsSignIn(page: Page, username: string, password: string) { + await page.getByText('Sign in with credentials').click(); + await page.getByLabel('Username').fill(username); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Sign in' }).click(); +} From 55b35e7c157aba5c7172b42c1fe0ab029f3be9e7 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:52:56 +0000 Subject: [PATCH 31/58] Update test/integration/auth/basic.spec.ts Signed-off-by: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> --- test/integration/auth/basic.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/auth/basic.spec.ts b/test/integration/auth/basic.spec.ts index 7e5ee0ac1ca..7d44389ec88 100644 --- a/test/integration/auth/basic.spec.ts +++ b/test/integration/auth/basic.spec.ts @@ -38,7 +38,7 @@ test('Must be authenticated with valid domain to view app', async ({ page }) => await tryCredentialsSignIn(page, 'mui', 'mui'); await expect(page).toHaveURL(/\/prod\/pages\/mypage/); - // Is not redirected when unauthenticated + // Is not redirected when authenticated await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL(/\/prod\/pages\/mypage/); From 9baabfe3cdfe646789dc02efa518dc3f3b431554 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:59:35 +0000 Subject: [PATCH 32/58] Better function name --- packages/toolpad-app/src/server/auth.ts | 2 +- packages/toolpad-app/src/server/toolpadAppServer.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index c41187d1fe2..4fc4f50d057 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -20,7 +20,7 @@ const SKIP_VERIFICATION_PROVIDERS: AuthProvider[] = [ 'credentials', ]; -export async function getHasAuthentication(project: ToolpadProject): Promise { +export async function getRequireAuthentication(project: ToolpadProject): Promise { const dom = await project.loadDom(); const app = appDom.getApp(dom); diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index 3a4f5b5e37e..6c7c242271b 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -12,7 +12,7 @@ import { RUNTIME_CONFIG_WINDOW_PROPERTY, INITIAL_STATE_WINDOW_PROPERTY } from '. import createRuntimeState from '../runtime/createRuntimeState'; import type { RuntimeConfig } from '../types'; import type { RuntimeState } from '../runtime'; -import { createAuthHandler, createRequireAuthMiddleware, getHasAuthentication } from './auth'; +import { createAuthHandler, createRequireAuthMiddleware, getRequireAuthentication } from './auth'; export interface PostProcessHtmlParams { config: RuntimeConfig; @@ -63,7 +63,7 @@ export async function createProdHandler(project: ToolpadProject) { basicAuthUnauthorized(res); }); - const hasAuthentication = await getHasAuthentication(project); + const hasAuthentication = await getRequireAuthentication(project); if (hasAuthentication) { const authHandler = createAuthHandler(project); handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); From ba3b0e388d239c74703fe462a50540a654635096 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:00:23 +0000 Subject: [PATCH 33/58] Forgot this --- packages/toolpad-app/src/server/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolpad-app/src/server/index.ts b/packages/toolpad-app/src/server/index.ts index 83ae1a35e5a..68a6d25434c 100644 --- a/packages/toolpad-app/src/server/index.ts +++ b/packages/toolpad-app/src/server/index.ts @@ -27,7 +27,7 @@ import { createRpcHandler } from './rpc'; import { APP_URL_WINDOW_PROPERTY } from '../constants'; import { createRpcServer as createProjectRpcServer } from './projectRpcServer'; import { createRpcServer as createRuntimeRpcServer } from './runtimeRpcServer'; -import { createAuthHandler, createRequireAuthMiddleware, getHasAuthentication } from './auth'; +import { createAuthHandler, createRequireAuthMiddleware, getRequireAuthentication } from './auth'; import.meta.url ??= url.pathToFileURL(__filename).toString(); const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -114,7 +114,7 @@ async function createDevHandler(project: ToolpadProject) { }), ); - const hasAuthentication = await getHasAuthentication(project); + const hasAuthentication = await getRequireAuthentication(project); if (hasAuthentication) { const authHandler = createAuthHandler(project); handler.use('/api/auth', express.urlencoded({ extended: true }), authHandler); From 9affd28368b3022a71aa035318714508a92b2c50 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:07:37 +0000 Subject: [PATCH 34/58] Continue merge --- docs/schemas/v1/definitions.json | 21 ------------------- packages/toolpad-app/src/server/auth.ts | 4 ++-- packages/toolpad-app/src/server/schema.ts | 6 ------ .../auth/{basic.spec.ts => domain.spec.ts} | 11 +++++++--- .../toolpad/.gitignore | 0 .../toolpad/application.yml | 0 .../toolpad/pages/mypage/page.yml | 0 .../integration/auth/fixture-roles/.gitignore | 1 - .../auth/fixture-roles/pages/page/page.yml | 7 ------- .../fixture-roles/toolpad/application.yml | 3 +-- test/integration/auth/roles.spec.ts | 12 ++++++----- 11 files changed, 18 insertions(+), 47 deletions(-) rename test/integration/auth/{basic.spec.ts => domain.spec.ts} (79%) rename test/integration/auth/{fixture-basic => fixture-domain}/toolpad/.gitignore (100%) rename test/integration/auth/{fixture-basic => fixture-domain}/toolpad/application.yml (100%) rename test/integration/auth/{fixture-basic => fixture-domain}/toolpad/pages/mypage/page.yml (100%) delete mode 100644 test/integration/auth/fixture-roles/.gitignore delete mode 100644 test/integration/auth/fixture-roles/pages/page/page.yml diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index 3e66a8adba6..37a0586b91a 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -96,27 +96,6 @@ ] }, "description": "Available roles for this application. These can be assigned to users." - }, - "roleMappings": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "propertyNames": { - "enum": [ - "github", - "google", - "azure-ad", - "credentials" - ] - }, - "description": "Role mapping definitions from authentication provider roles to Toolpad roles." } }, "additionalProperties": false, diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 469dec19adf..90a2837111a 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -139,7 +139,7 @@ export function createAuthHandler(project: ToolpadProject): Router { tenantId: process.env.TOOLPAD_AZURE_AD_TENANT_ID, }); - const mockCredentialsProvider = CredentialsProvider({ + const credentialsProvider = CredentialsProvider({ name: 'Credentials', async authorize({ username, password }) { if (process.env.NODE_ENV !== 'test') { @@ -172,7 +172,7 @@ export function createAuthHandler(project: ToolpadProject): Router { error: `${base}/signin`, // Error code passed in query string as ?error= verifyRequest: base, }, - providers: [githubProvider, googleProvider, azureADProvider, mockCredentialsProvider], + providers: [githubProvider, googleProvider, azureADProvider, credentialsProvider], secret: process.env.TOOLPAD_AUTH_SECRET, trustHost: true, callbacks: { diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index cf05cdfe874..d075647be1b 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -305,12 +305,6 @@ export const applicationSchema = toolpadObjectSchema( ) .optional() .describe('Available roles for this application. These can be assigned to users.'), - roleMappings: z - .record(authProviderSchema, z.record(z.array(z.string()))) - .optional() - .describe( - 'Role mapping definitions from authentication provider roles to Toolpad roles.', - ), }) .optional() .describe('Authorization configuration for this application.'), diff --git a/test/integration/auth/basic.spec.ts b/test/integration/auth/domain.spec.ts similarity index 79% rename from test/integration/auth/basic.spec.ts rename to test/integration/auth/domain.spec.ts index 7d44389ec88..2d353a39b02 100644 --- a/test/integration/auth/basic.spec.ts +++ b/test/integration/auth/domain.spec.ts @@ -13,7 +13,7 @@ test.use({ test.use({ projectConfig: { - template: path.resolve(currentDirectory, './fixture-basic'), + template: path.resolve(currentDirectory, './fixture-domain'), }, localAppConfig: { cmd: 'start', @@ -24,11 +24,15 @@ test.use({ }, }); -test('Must be authenticated with valid domain to view app', async ({ page }) => { +test('Must be authenticated with valid domain to access app', async ({ page, request }) => { // Is redirected when unauthenticated await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL(/\/prod\/signin/); + // Access is blocked to API route + const res = await request.post('/prod/api/data/page/hello'); + expect(res.status()).toBe(401); + // Sign in with invalid domain await tryCredentialsSignIn(page, 'test', 'test'); await expect(page).toHaveURL(/\/prod\/signin/); @@ -37,6 +41,7 @@ test('Must be authenticated with valid domain to view app', async ({ page }) => // Sign in with valid domain await tryCredentialsSignIn(page, 'mui', 'mui'); await expect(page).toHaveURL(/\/prod\/pages\/mypage/); + await expect(page.getByText('message: hello world')).toBeVisible(); // Is not redirected when authenticated await page.goto('/prod/pages/mypage'); @@ -51,4 +56,4 @@ test('Must be authenticated with valid domain to view app', async ({ page }) => // Is redirected when unauthenticated await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL(/\/prod\/signin/); -}); +}); \ No newline at end of file diff --git a/test/integration/auth/fixture-basic/toolpad/.gitignore b/test/integration/auth/fixture-domain/toolpad/.gitignore similarity index 100% rename from test/integration/auth/fixture-basic/toolpad/.gitignore rename to test/integration/auth/fixture-domain/toolpad/.gitignore diff --git a/test/integration/auth/fixture-basic/toolpad/application.yml b/test/integration/auth/fixture-domain/toolpad/application.yml similarity index 100% rename from test/integration/auth/fixture-basic/toolpad/application.yml rename to test/integration/auth/fixture-domain/toolpad/application.yml diff --git a/test/integration/auth/fixture-basic/toolpad/pages/mypage/page.yml b/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml similarity index 100% rename from test/integration/auth/fixture-basic/toolpad/pages/mypage/page.yml rename to test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml diff --git a/test/integration/auth/fixture-roles/.gitignore b/test/integration/auth/fixture-roles/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/integration/auth/fixture-roles/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/integration/auth/fixture-roles/pages/page/page.yml b/test/integration/auth/fixture-roles/pages/page/page.yml deleted file mode 100644 index 45c4b67e93c..00000000000 --- a/test/integration/auth/fixture-roles/pages/page/page.yml +++ /dev/null @@ -1,7 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.45/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - id: Qz9ETeM - title: Default page diff --git a/test/integration/auth/fixture-roles/toolpad/application.yml b/test/integration/auth/fixture-roles/toolpad/application.yml index c67db9006b5..35596fba5e8 100644 --- a/test/integration/auth/fixture-roles/toolpad/application.yml +++ b/test/integration/auth/fixture-roles/toolpad/application.yml @@ -2,10 +2,9 @@ apiVersion: v1 kind: application spec: authentication: - providers: [{ provider: credentials }] + providers: [{ provider: credentials }, { roles: [{ source: 'admin', target: ['mockAdmin', 'god']}]}] restrictedDomains: [] authorization: { roles: [{ name: admin, description: A very important person. }], - roleMappings: { credentials: { admin: ['mock-admin', 'god'] } }, } diff --git a/test/integration/auth/roles.spec.ts b/test/integration/auth/roles.spec.ts index 0974b9bcf7a..cd44896b5fa 100644 --- a/test/integration/auth/roles.spec.ts +++ b/test/integration/auth/roles.spec.ts @@ -24,7 +24,7 @@ test.use({ }, }); -test.only('Must have required roles to view pages', async ({ page }) => { +test.only('Must have required roles to access pages', async ({ page, request }) => { await page.goto('/prod/signin'); // Sign in without admin role @@ -32,13 +32,15 @@ test.only('Must have required roles to view pages', async ({ page }) => { await expect(page.getByText('Admin Page')).toBeHidden(); + // Access is blocked to API route + const res = await request.post('/prod/api/data/adminpage/hello'); + expect(res.status()).toBe(401); + // Sign in with admin role await page.getByText('Miss Test').click(); await page.getByText('Sign out').click(); await tryCredentialsSignIn(page, 'admin', 'admin'); await expect(page.getByText('Admin Page')).toBeVisible(); - await expect( - page.getByText('I just want to tell all the admins out there to never stop administrating.'), - ).toBeVisible(); -}); + await expect(page.getByText('message: hello world')).toBeVisible(); +}); \ No newline at end of file From e7b293104ef4dde990f4e6e943537f107999393f Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:21:39 +0000 Subject: [PATCH 35/58] Add some refactors from other PR --- packages/toolpad-app/src/server/auth.ts | 46 +++++++++++++------ .../toolpad/pages/mypage/page.yml | 25 ++++++---- .../toolpad/resources/functions.ts | 3 ++ .../fixture-roles/toolpad/application.yml | 9 ++-- .../toolpad/pages/adminpage/page.yml | 16 +++++-- .../toolpad/pages/publicpage/page.yml | 2 +- .../toolpad/resources/functions.ts | 3 ++ test/integration/auth/roles.spec.ts | 4 -- 8 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 test/integration/auth/fixture-domain/toolpad/resources/functions.ts create mode 100644 test/integration/auth/fixture-roles/toolpad/resources/functions.ts diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 90a2837111a..469a2c08951 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -4,14 +4,14 @@ import GithubProvider, { GitHubEmail, GitHubProfile } from '@auth/core/providers import GoogleProvider from '@auth/core/providers/google'; import AzureADProvider from '@auth/core/providers/azure-ad'; import CredentialsProvider from '@auth/core/providers/credentials'; -import { getToken } from '@auth/core/jwt'; import { AuthConfig, TokenSet } from '@auth/core/types'; import { OAuthConfig } from '@auth/core/providers'; import chalk from 'chalk'; import { asyncHandler } from '../utils/express'; import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; -import { ToolpadProject } from './localMode'; +import type { ToolpadProject } from './localMode'; import * as appDom from '@mui/toolpad-core/appDom'; +import { JWT, getToken } from '@auth/core/jwt'; const SKIP_VERIFICATION_PROVIDERS: appDom.AuthProvider[] = [ // Azure AD should be fine to skip as the user has to belong to the organization to sign in @@ -19,6 +19,17 @@ const SKIP_VERIFICATION_PROVIDERS: appDom.AuthProvider[] = [ 'credentials', ]; +async function getAuthProviders( + project: Pick, +): Promise { + const dom = await project.loadDom(); + const app = appDom.getApp(dom); + + const authProviders = app.attributes.authentication?.providers ?? []; + + return authProviders; +} + export async function getRequireAuthentication(project: ToolpadProject): Promise { const dom = await project.loadDom(); const app = appDom.getApp(dom); @@ -28,6 +39,23 @@ export async function getRequireAuthentication(project: ToolpadProject): Promise return authProviders.length > 0; } +export async function getUserToken(req: express.Request): Promise { + let token = null; + if (process.env.TOOLPAD_AUTH_SECRET) { + const request = adaptRequestFromExpressToFetch(req); + + // @TODO: Library types are wrong as salt should not be required, remove once fixed + // Github discussion: https://github.com/nextauthjs/next-auth/discussions/9133 + // @ts-ignore + token = await getToken({ + req: request, + secret: process.env.TOOLPAD_AUTH_SECRET, + }); + } + + return token; +} + function getMappedRoles( roles: string[], allRoles: string[], @@ -281,19 +309,7 @@ export async function createRequireAuthMiddleware(project: ToolpadProject) { let isAuthorized = true; if ((!project.options.dev || isPageRequest) && req.originalUrl.split('?')[0] !== signInPath) { - const request = adaptRequestFromExpressToFetch(req); - - let token; - if (process.env.TOOLPAD_AUTH_SECRET) { - // @TODO: Library types are wrong as salt should not be required, remove once fixed - // Github discussion: https://github.com/nextauthjs/next-auth/discussions/9133 - // @ts-ignore - token = await getToken({ - req: request, - secret: process.env.TOOLPAD_AUTH_SECRET, - }); - } - + const token = await getUserToken(req); if (!token) { isAuthorized = false; } diff --git a/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml b/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml index a3fdfc98075..31ebc4c0340 100644 --- a/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml +++ b/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml @@ -1,13 +1,20 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page - apiVersion: v1 kind: page spec: - displayName: My Page - title: mypage - display: shell + id: 5q1xd0t + title: Page content: - - component: Text - name: text - props: - value: Hello world + - component: PageRow + name: pageRow + children: + - component: Text + name: typography + props: + value: + $$jsExpression: | + `message: ${hello.data.message}` + queries: + - name: hello + query: + function: hello + kind: local diff --git a/test/integration/auth/fixture-domain/toolpad/resources/functions.ts b/test/integration/auth/fixture-domain/toolpad/resources/functions.ts new file mode 100644 index 00000000000..7951b807142 --- /dev/null +++ b/test/integration/auth/fixture-domain/toolpad/resources/functions.ts @@ -0,0 +1,3 @@ +export async function hello() { + return { message: 'hello world' }; +} diff --git a/test/integration/auth/fixture-roles/toolpad/application.yml b/test/integration/auth/fixture-roles/toolpad/application.yml index 35596fba5e8..5cc9fb454cb 100644 --- a/test/integration/auth/fixture-roles/toolpad/application.yml +++ b/test/integration/auth/fixture-roles/toolpad/application.yml @@ -2,9 +2,8 @@ apiVersion: v1 kind: application spec: authentication: - providers: [{ provider: credentials }, { roles: [{ source: 'admin', target: ['mockAdmin', 'god']}]}] - restrictedDomains: [] + providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] authorization: - { - roles: [{ name: admin, description: A very important person. }], - } + roles: + - name: admin + description: 'A very important person.' diff --git a/test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml b/test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml index 0d770a2bf65..e9faea13e80 100644 --- a/test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml +++ b/test/integration/auth/fixture-roles/toolpad/pages/adminpage/page.yml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.45/docs/schemas/v1/definitions.json#properties/Page +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page apiVersion: v1 kind: page @@ -12,7 +12,15 @@ spec: displayName: Admin Page content: - component: Text - name: text + name: typography + layout: + columnSize: 1 props: - value: I just want to tell all the admins out there to never stop - administrating. + value: + $$jsExpression: | + `message: ${hello.data.message}` + queries: + - name: hello + query: + function: hello + kind: local diff --git a/test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml b/test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml index e92b9373880..21867f13af7 100644 --- a/test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml +++ b/test/integration/auth/fixture-roles/toolpad/pages/publicpage/page.yml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.45/docs/schemas/v1/definitions.json#properties/Page +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page apiVersion: v1 kind: page diff --git a/test/integration/auth/fixture-roles/toolpad/resources/functions.ts b/test/integration/auth/fixture-roles/toolpad/resources/functions.ts new file mode 100644 index 00000000000..7951b807142 --- /dev/null +++ b/test/integration/auth/fixture-roles/toolpad/resources/functions.ts @@ -0,0 +1,3 @@ +export async function hello() { + return { message: 'hello world' }; +} diff --git a/test/integration/auth/roles.spec.ts b/test/integration/auth/roles.spec.ts index cd44896b5fa..408cfb1604e 100644 --- a/test/integration/auth/roles.spec.ts +++ b/test/integration/auth/roles.spec.ts @@ -32,10 +32,6 @@ test.only('Must have required roles to access pages', async ({ page, request }) await expect(page.getByText('Admin Page')).toBeHidden(); - // Access is blocked to API route - const res = await request.post('/prod/api/data/adminpage/hello'); - expect(res.status()).toBe(401); - // Sign in with admin role await page.getByText('Miss Test').click(); await page.getByText('Sign out').click(); From 27f8e3e19c893b8dbe8db2bad46ad86726b282f8 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:22:25 +0000 Subject: [PATCH 36/58] Disable feature flag --- packages/toolpad-app/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolpad-app/src/constants.ts b/packages/toolpad-app/src/constants.ts index 5d0b6d21566..9a8e5503061 100644 --- a/packages/toolpad-app/src/constants.ts +++ b/packages/toolpad-app/src/constants.ts @@ -21,4 +21,4 @@ export const VERSION_CHECK_INTERVAL = 1000 * 60 * 10; // TODO: Remove once global functions UI is ready export const FEATURE_FLAG_GLOBAL_FUNCTIONS = false; -export const FEATURE_FLAG_AUTHORIZATION = true; +export const FEATURE_FLAG_AUTHORIZATION = false; From 3d7ad43aac590f271daa6fb10e0c8a0e1637ff6b Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:23:46 +0000 Subject: [PATCH 37/58] Fix tests --- packages/toolpad-app/package.json | 2 +- packages/toolpad-app/src/server/auth.ts | 8 ++---- packages/toolpad-app/src/server/schema.ts | 4 +-- .../AppEditor/AppAuthorizationEditor.tsx | 2 +- pnpm-lock.yaml | 10 +++---- test/integration/auth/roles.spec.ts | 2 +- .../tmp-0xS4E0/fixture/toolpad/.gitignore | 1 + .../fixture/toolpad/application.yml | 9 +++++++ .../fixture/toolpad/pages/adminpage/page.yml | 26 +++++++++++++++++++ .../fixture/toolpad/pages/publicpage/page.yml | 15 +++++++++++ .../fixture/toolpad/resources/functions.ts | 3 +++ .../tmp-8DcakT/fixture/toolpad/.gitignore | 1 + .../fixture/toolpad/application.yml | 9 +++++++ .../fixture/toolpad/pages/adminpage/page.yml | 26 +++++++++++++++++++ .../fixture/toolpad/pages/publicpage/page.yml | 15 +++++++++++ .../fixture/toolpad/resources/functions.ts | 3 +++ .../tmp-FRuzyq/fixture/toolpad/.gitignore | 1 + .../fixture/toolpad/application.yml | 9 +++++++ .../fixture/toolpad/pages/adminpage/page.yml | 26 +++++++++++++++++++ .../fixture/toolpad/pages/publicpage/page.yml | 15 +++++++++++ .../fixture/toolpad/resources/functions.ts | 3 +++ .../tmp-ZLdEe7/fixture/toolpad/.gitignore | 1 + .../fixture/toolpad/application.yml | 9 +++++++ .../fixture/toolpad/pages/adminpage/page.yml | 26 +++++++++++++++++++ .../fixture/toolpad/pages/publicpage/page.yml | 15 +++++++++++ .../fixture/toolpad/resources/functions.ts | 3 +++ .../tmp-z2mqKy/fixture/toolpad/.gitignore | 1 + .../fixture/toolpad/application.yml | 9 +++++++ .../fixture/toolpad/pages/adminpage/page.yml | 26 +++++++++++++++++++ .../fixture/toolpad/pages/publicpage/page.yml | 15 +++++++++++ .../fixture/toolpad/resources/functions.ts | 3 +++ 31 files changed, 281 insertions(+), 17 deletions(-) create mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore create mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml create mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml create mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml create mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts create mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore create mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/application.yml create mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml create mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml create mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts create mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore create mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml create mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml create mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml create mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts create mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore create mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml create mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml create mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml create mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts create mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore create mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml create mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml create mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml create mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts diff --git a/packages/toolpad-app/package.json b/packages/toolpad-app/package.json index 14b2cbd0eac..772ed362427 100644 --- a/packages/toolpad-app/package.json +++ b/packages/toolpad-app/package.json @@ -41,7 +41,7 @@ } }, "dependencies": { - "@auth/core": "0.24.0", + "@auth/core": "0.20.0", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.3", "@emotion/server": "11.11.0", diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 469a2c08951..225faa0dbf7 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -31,11 +31,7 @@ async function getAuthProviders( } export async function getRequireAuthentication(project: ToolpadProject): Promise { - const dom = await project.loadDom(); - const app = appDom.getApp(dom); - - const authProviders = app.attributes.authentication?.providers ?? []; - + const authProviders = await getAuthProviders(project); return authProviders.length > 0; } @@ -250,7 +246,7 @@ export function createAuthHandler(project: ToolpadProject): Router { if (account?.provider === 'credentials') { const roleMappings = authentication?.providers?.find( - (providerConfig) => providerConfig.provider === 'azure-ad', + (providerConfig) => providerConfig.provider === 'credentials', )?.roles ?? []; token.roles = getMappedRoles(user?.roles ?? [], roleNames, roleMappings); diff --git a/packages/toolpad-app/src/server/schema.ts b/packages/toolpad-app/src/server/schema.ts index d075647be1b..00358f61552 100644 --- a/packages/toolpad-app/src/server/schema.ts +++ b/packages/toolpad-app/src/server/schema.ts @@ -256,8 +256,6 @@ elementSchema = baseElementSchema }) .describe('The instance of a component. Used to build user interfaces in pages.'); -const authProviderSchema = z.enum(['github', 'google', 'azure-ad', 'credentials']); - export const applicationSchema = toolpadObjectSchema( 'application', z.object({ @@ -267,7 +265,7 @@ export const applicationSchema = toolpadObjectSchema( .array( z.object({ provider: z - .enum(['github', 'google', 'azure-ad']) + .enum(['github', 'google', 'azure-ad', 'credentials']) .describe('Unique identifier for this authentication provider.'), roles: z .array( diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 9f95426b21e..8235558e92c 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -58,7 +58,7 @@ const AUTH_PROVIDER_OPTIONS = new Map([ icon: , hasRoles: true, }, - ], + ] ]); export function AppAuthenticationEditor() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2743654f721..97fae249726 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,8 +484,8 @@ importers: packages/toolpad-app: dependencies: '@auth/core': - specifier: 0.24.0 - version: 0.24.0 + specifier: 0.20.0 + version: 0.20.0 '@emotion/cache': specifier: 11.11.0 version: 11.11.0 @@ -1227,8 +1227,8 @@ packages: engines: {node: '>=16.0.0'} dev: true - /@auth/core@0.24.0: - resolution: {integrity: sha512-wwTyapljg4ydyvtQRXSeOaj6nBVSvPkVoXws6i+/vPfINxz4lo9UuLifPLqW7iO72/f4Ttaez0g3XA42VtKQ8A==} + /@auth/core@0.20.0: + resolution: {integrity: sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==} peerDependencies: nodemailer: ^6.8.0 peerDependenciesMeta: @@ -15251,4 +15251,4 @@ packages: - debug - encoding - supports-color - - utf-8-validate \ No newline at end of file + - utf-8-validate diff --git a/test/integration/auth/roles.spec.ts b/test/integration/auth/roles.spec.ts index 408cfb1604e..184e284bdf9 100644 --- a/test/integration/auth/roles.spec.ts +++ b/test/integration/auth/roles.spec.ts @@ -24,7 +24,7 @@ test.use({ }, }); -test.only('Must have required roles to access pages', async ({ page, request }) => { +test('Must have required roles to access pages', async ({ page, request }) => { await page.goto('/prod/signin'); // Sign in without admin role diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore b/test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml b/test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml new file mode 100644 index 00000000000..5cc9fb454cb --- /dev/null +++ b/test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] + authorization: + roles: + - name: admin + description: 'A very important person.' diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml new file mode 100644 index 00000000000..e9faea13e80 --- /dev/null +++ b/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: adminpage + display: shell + authorization: + allowAll: false + allowedRoles: + - admin + displayName: Admin Page + content: + - component: Text + name: typography + layout: + columnSize: 1 + props: + value: + $$jsExpression: | + `message: ${hello.data.message}` + queries: + - name: hello + query: + function: hello + kind: local diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml new file mode 100644 index 00000000000..21867f13af7 --- /dev/null +++ b/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: publicpage + display: shell + authorization: + allowAll: true + displayName: Public Page + content: + - component: Text + name: text + props: + value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts new file mode 100644 index 00000000000..7951b807142 --- /dev/null +++ b/test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts @@ -0,0 +1,3 @@ +export async function hello() { + return { message: 'hello world' }; +} diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore b/test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/application.yml b/test/playwright/tmp-8DcakT/fixture/toolpad/application.yml new file mode 100644 index 00000000000..5cc9fb454cb --- /dev/null +++ b/test/playwright/tmp-8DcakT/fixture/toolpad/application.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] + authorization: + roles: + - name: admin + description: 'A very important person.' diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml new file mode 100644 index 00000000000..e9faea13e80 --- /dev/null +++ b/test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: adminpage + display: shell + authorization: + allowAll: false + allowedRoles: + - admin + displayName: Admin Page + content: + - component: Text + name: typography + layout: + columnSize: 1 + props: + value: + $$jsExpression: | + `message: ${hello.data.message}` + queries: + - name: hello + query: + function: hello + kind: local diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml new file mode 100644 index 00000000000..21867f13af7 --- /dev/null +++ b/test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: publicpage + display: shell + authorization: + allowAll: true + displayName: Public Page + content: + - component: Text + name: text + props: + value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts new file mode 100644 index 00000000000..7951b807142 --- /dev/null +++ b/test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts @@ -0,0 +1,3 @@ +export async function hello() { + return { message: 'hello world' }; +} diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore b/test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml b/test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml new file mode 100644 index 00000000000..5cc9fb454cb --- /dev/null +++ b/test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] + authorization: + roles: + - name: admin + description: 'A very important person.' diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml new file mode 100644 index 00000000000..e9faea13e80 --- /dev/null +++ b/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: adminpage + display: shell + authorization: + allowAll: false + allowedRoles: + - admin + displayName: Admin Page + content: + - component: Text + name: typography + layout: + columnSize: 1 + props: + value: + $$jsExpression: | + `message: ${hello.data.message}` + queries: + - name: hello + query: + function: hello + kind: local diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml new file mode 100644 index 00000000000..21867f13af7 --- /dev/null +++ b/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: publicpage + display: shell + authorization: + allowAll: true + displayName: Public Page + content: + - component: Text + name: text + props: + value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts new file mode 100644 index 00000000000..7951b807142 --- /dev/null +++ b/test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts @@ -0,0 +1,3 @@ +export async function hello() { + return { message: 'hello world' }; +} diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore b/test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml b/test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml new file mode 100644 index 00000000000..5cc9fb454cb --- /dev/null +++ b/test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] + authorization: + roles: + - name: admin + description: 'A very important person.' diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml new file mode 100644 index 00000000000..e9faea13e80 --- /dev/null +++ b/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: adminpage + display: shell + authorization: + allowAll: false + allowedRoles: + - admin + displayName: Admin Page + content: + - component: Text + name: typography + layout: + columnSize: 1 + props: + value: + $$jsExpression: | + `message: ${hello.data.message}` + queries: + - name: hello + query: + function: hello + kind: local diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml new file mode 100644 index 00000000000..21867f13af7 --- /dev/null +++ b/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: publicpage + display: shell + authorization: + allowAll: true + displayName: Public Page + content: + - component: Text + name: text + props: + value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts new file mode 100644 index 00000000000..7951b807142 --- /dev/null +++ b/test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts @@ -0,0 +1,3 @@ +export async function hello() { + return { message: 'hello world' }; +} diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore b/test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore new file mode 100644 index 00000000000..5f1e4d07bfd --- /dev/null +++ b/test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore @@ -0,0 +1 @@ +.generated diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml b/test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml new file mode 100644 index 00000000000..5cc9fb454cb --- /dev/null +++ b/test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: application +spec: + authentication: + providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] + authorization: + roles: + - name: admin + description: 'A very important person.' diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml new file mode 100644 index 00000000000..e9faea13e80 --- /dev/null +++ b/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: adminpage + display: shell + authorization: + allowAll: false + allowedRoles: + - admin + displayName: Admin Page + content: + - component: Text + name: typography + layout: + columnSize: 1 + props: + value: + $$jsExpression: | + `message: ${hello.data.message}` + queries: + - name: hello + query: + function: hello + kind: local diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml new file mode 100644 index 00000000000..21867f13af7 --- /dev/null +++ b/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page + +apiVersion: v1 +kind: page +spec: + title: publicpage + display: shell + authorization: + allowAll: true + displayName: Public Page + content: + - component: Text + name: text + props: + value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts new file mode 100644 index 00000000000..7951b807142 --- /dev/null +++ b/test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts @@ -0,0 +1,3 @@ +export async function hello() { + return { message: 'hello world' }; +} From 833feb74ff4f07dcc8c05fd1a2b825f7de2d21af Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:31:22 +0000 Subject: [PATCH 38/58] Update @auth/core --- packages/toolpad-app/package.json | 2 +- packages/toolpad-app/src/server/auth.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/toolpad-app/package.json b/packages/toolpad-app/package.json index 772ed362427..14b2cbd0eac 100644 --- a/packages/toolpad-app/package.json +++ b/packages/toolpad-app/package.json @@ -41,7 +41,7 @@ } }, "dependencies": { - "@auth/core": "0.20.0", + "@auth/core": "0.24.0", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.3", "@emotion/server": "11.11.0", diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 225faa0dbf7..e59bd709190 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -190,6 +190,7 @@ export function createAuthHandler(project: ToolpadProject): Router { }); const authConfig: AuthConfig = { + basePath: base, pages: { signIn: `${base}/signin`, signOut: base, From e079ac1859ec5115ca144b7aa8d3972bcbdf3f12 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:38:50 +0000 Subject: [PATCH 39/58] Run install --- docs/schemas/v1/definitions.json | 7 +------ pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/schemas/v1/definitions.json b/docs/schemas/v1/definitions.json index 37a0586b91a..fb61e673410 100644 --- a/docs/schemas/v1/definitions.json +++ b/docs/schemas/v1/definitions.json @@ -27,12 +27,7 @@ "properties": { "provider": { "type": "string", - "enum": [ - "github", - "google", - "azure-ad", - "credentials" - ], + "enum": ["github", "google", "azure-ad", "credentials"], "description": "Unique identifier for this authentication provider." }, "roles": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97fae249726..94e85ce4e45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,8 +484,8 @@ importers: packages/toolpad-app: dependencies: '@auth/core': - specifier: 0.20.0 - version: 0.20.0 + specifier: 0.24.0 + version: 0.24.0 '@emotion/cache': specifier: 11.11.0 version: 11.11.0 @@ -1227,8 +1227,8 @@ packages: engines: {node: '>=16.0.0'} dev: true - /@auth/core@0.20.0: - resolution: {integrity: sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==} + /@auth/core@0.24.0: + resolution: {integrity: sha512-wwTyapljg4ydyvtQRXSeOaj6nBVSvPkVoXws6i+/vPfINxz4lo9UuLifPLqW7iO72/f4Ttaez0g3XA42VtKQ8A==} peerDependencies: nodemailer: ^6.8.0 peerDependenciesMeta: From f799ce523a7ae02917e70b7973f48b9ef2ef8a35 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:47:09 +0000 Subject: [PATCH 40/58] Lint fixins --- packages/toolpad-app/src/server/auth.ts | 28 ++++++++++++------------- test/integration/auth/roles.spec.ts | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index e59bd709190..5eff5b974f1 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -7,11 +7,11 @@ import CredentialsProvider from '@auth/core/providers/credentials'; import { AuthConfig, TokenSet } from '@auth/core/types'; import { OAuthConfig } from '@auth/core/providers'; import chalk from 'chalk'; +import * as appDom from '@mui/toolpad-core/appDom'; +import { JWT, getToken } from '@auth/core/jwt'; import { asyncHandler } from '../utils/express'; import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; import type { ToolpadProject } from './localMode'; -import * as appDom from '@mui/toolpad-core/appDom'; -import { JWT, getToken } from '@auth/core/jwt'; const SKIP_VERIFICATION_PROVIDERS: appDom.AuthProvider[] = [ // Azure AD should be fine to skip as the user has to belong to the organization to sign in @@ -55,14 +55,12 @@ export async function getUserToken(req: express.Request): Promise { function getMappedRoles( roles: string[], allRoles: string[], - roleMappings: appDom.AuthProviderConfig['roles'] = [] + roleMappings: appDom.AuthProviderConfig['roles'] = [], ): string[] { return (roles ?? []).flatMap((providerRole) => allRoles .filter((role) => { - const targetRoleMapping = roleMappings.find( - (roleMapping) => roleMapping.target === role, - ); + const targetRoleMapping = roleMappings.find((roleMapping) => roleMapping.target === role); return targetRoleMapping ? targetRoleMapping.source.includes(providerRole) @@ -233,10 +231,11 @@ export function createAuthHandler(project: ToolpadProject): Router { const authentication = app.attributes.authentication ?? {}; if (account?.provider === 'azure-ad' && account.id_token) { - const roleMappings = authentication?.providers?.find( - (providerConfig) => providerConfig.provider === 'azure-ad', - )?.roles ?? []; - + const roleMappings = + authentication?.providers?.find( + (providerConfig) => providerConfig.provider === 'azure-ad', + )?.roles ?? []; + const [, payload] = account.id_token.split('.'); const idToken: { roles?: string[] } = JSON.parse( Buffer.from(payload, 'base64').toString('utf8'), @@ -246,10 +245,11 @@ export function createAuthHandler(project: ToolpadProject): Router { } if (account?.provider === 'credentials') { - const roleMappings = authentication?.providers?.find( - (providerConfig) => providerConfig.provider === 'credentials', - )?.roles ?? []; - + const roleMappings = + authentication?.providers?.find( + (providerConfig) => providerConfig.provider === 'credentials', + )?.roles ?? []; + token.roles = getMappedRoles(user?.roles ?? [], roleNames, roleMappings); } diff --git a/test/integration/auth/roles.spec.ts b/test/integration/auth/roles.spec.ts index 184e284bdf9..6c7d2fb3d82 100644 --- a/test/integration/auth/roles.spec.ts +++ b/test/integration/auth/roles.spec.ts @@ -24,7 +24,7 @@ test.use({ }, }); -test('Must have required roles to access pages', async ({ page, request }) => { +test('Must have required roles to access pages', async ({ page }) => { await page.goto('/prod/signin'); // Sign in without admin role @@ -39,4 +39,4 @@ test('Must have required roles to access pages', async ({ page, request }) => { await expect(page.getByText('Admin Page')).toBeVisible(); await expect(page.getByText('message: hello world')).toBeVisible(); -}); \ No newline at end of file +}); From d4869b1db9ecc5279ebb1f4ba86a346a5ad5c678 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:50:44 +0000 Subject: [PATCH 41/58] Prettier --- packages/toolpad-app/src/runtime/ToolpadApp.tsx | 5 +---- .../src/toolpad/AppEditor/AppAuthorizationEditor.tsx | 7 ++++--- test/integration/auth/domain.spec.ts | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index 7d8ae0f87c1..5b4a2d9de2f 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -1596,10 +1596,7 @@ function ToolpadAppLayout({ dom, basename, clipped }: ToolpadAppLayoutProps) { clipped={clipped} basename={basename} > - + ); } diff --git a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx index 8235558e92c..e74313079dc 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/AppAuthorizationEditor.tsx @@ -58,7 +58,7 @@ const AUTH_PROVIDER_OPTIONS = new Map([ icon: , hasRoles: true, }, - ] + ], ]); export function AppAuthenticationEditor() { @@ -668,8 +668,9 @@ export default function AppAuthorizationDialog({ open, onClose }: AppAuthorizati .map((providerConfig) => providerConfig.provider); return [...AUTH_PROVIDER_OPTIONS].filter( - ([optionKey, { hasRoles }]) => hasRoles && authProviders.includes(optionKey as appDom.AuthProvider), - ) + ([optionKey, { hasRoles }]) => + hasRoles && authProviders.includes(optionKey as appDom.AuthProvider), + ); }, [dom]); return ( diff --git a/test/integration/auth/domain.spec.ts b/test/integration/auth/domain.spec.ts index 2d353a39b02..22a3d1d97b5 100644 --- a/test/integration/auth/domain.spec.ts +++ b/test/integration/auth/domain.spec.ts @@ -56,4 +56,4 @@ test('Must be authenticated with valid domain to access app', async ({ page, req // Is redirected when unauthenticated await page.goto('/prod/pages/mypage'); await expect(page).toHaveURL(/\/prod\/signin/); -}); \ No newline at end of file +}); From 6f9c719e645b13c5159f4401422b0e8eb7f66024 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:18:06 +0000 Subject: [PATCH 42/58] Revert @auth/core version --- packages/toolpad-app/package.json | 2 +- packages/toolpad-app/src/server/auth.ts | 1 - pnpm-lock.yaml | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/toolpad-app/package.json b/packages/toolpad-app/package.json index 14b2cbd0eac..772ed362427 100644 --- a/packages/toolpad-app/package.json +++ b/packages/toolpad-app/package.json @@ -41,7 +41,7 @@ } }, "dependencies": { - "@auth/core": "0.24.0", + "@auth/core": "0.20.0", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.3", "@emotion/server": "11.11.0", diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index 5eff5b974f1..a1d1ce9ce6d 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -188,7 +188,6 @@ export function createAuthHandler(project: ToolpadProject): Router { }); const authConfig: AuthConfig = { - basePath: base, pages: { signIn: `${base}/signin`, signOut: base, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94e85ce4e45..97fae249726 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,8 +484,8 @@ importers: packages/toolpad-app: dependencies: '@auth/core': - specifier: 0.24.0 - version: 0.24.0 + specifier: 0.20.0 + version: 0.20.0 '@emotion/cache': specifier: 11.11.0 version: 11.11.0 @@ -1227,8 +1227,8 @@ packages: engines: {node: '>=16.0.0'} dev: true - /@auth/core@0.24.0: - resolution: {integrity: sha512-wwTyapljg4ydyvtQRXSeOaj6nBVSvPkVoXws6i+/vPfINxz4lo9UuLifPLqW7iO72/f4Ttaez0g3XA42VtKQ8A==} + /@auth/core@0.20.0: + resolution: {integrity: sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==} peerDependencies: nodemailer: ^6.8.0 peerDependenciesMeta: From 3b8a69e6d721dad8fb8ce5493804040e8655cffd Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:46:42 +0000 Subject: [PATCH 43/58] Remove all temporary fixtures --- .../tmp-0xS4E0/fixture/toolpad/.gitignore | 1 - .../fixture/toolpad/application.yml | 9 ------- .../fixture/toolpad/pages/adminpage/page.yml | 26 ------------------- .../fixture/toolpad/pages/publicpage/page.yml | 15 ----------- .../fixture/toolpad/resources/functions.ts | 3 --- .../tmp-6n4uK4/fixture/toolpad/.gitignore | 1 - .../fixture/toolpad/application.yml | 7 ----- .../fixture/toolpad/pages/mypage/page.yml | 13 ---------- .../tmp-8DcakT/fixture/toolpad/.gitignore | 1 - .../fixture/toolpad/application.yml | 9 ------- .../fixture/toolpad/pages/adminpage/page.yml | 26 ------------------- .../fixture/toolpad/pages/publicpage/page.yml | 15 ----------- .../fixture/toolpad/resources/functions.ts | 3 --- .../tmp-FRuzyq/fixture/toolpad/.gitignore | 1 - .../fixture/toolpad/application.yml | 9 ------- .../fixture/toolpad/pages/adminpage/page.yml | 26 ------------------- .../fixture/toolpad/pages/publicpage/page.yml | 15 ----------- .../fixture/toolpad/resources/functions.ts | 3 --- .../tmp-ZLdEe7/fixture/toolpad/.gitignore | 1 - .../fixture/toolpad/application.yml | 9 ------- .../fixture/toolpad/pages/adminpage/page.yml | 26 ------------------- .../fixture/toolpad/pages/publicpage/page.yml | 15 ----------- .../fixture/toolpad/resources/functions.ts | 3 --- .../tmp-z2mqKy/fixture/toolpad/.gitignore | 1 - .../fixture/toolpad/application.yml | 9 ------- .../fixture/toolpad/pages/adminpage/page.yml | 26 ------------------- .../fixture/toolpad/pages/publicpage/page.yml | 15 ----------- .../fixture/toolpad/resources/functions.ts | 3 --- 28 files changed, 291 deletions(-) delete mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore delete mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml delete mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml delete mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml delete mode 100644 test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts delete mode 100644 test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore delete mode 100644 test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml delete mode 100644 test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml delete mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore delete mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/application.yml delete mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml delete mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml delete mode 100644 test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts delete mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore delete mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml delete mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml delete mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml delete mode 100644 test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts delete mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore delete mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml delete mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml delete mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml delete mode 100644 test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts delete mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore delete mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml delete mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml delete mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml delete mode 100644 test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore b/test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/playwright/tmp-0xS4E0/fixture/toolpad/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml b/test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml deleted file mode 100644 index 5cc9fb454cb..00000000000 --- a/test/playwright/tmp-0xS4E0/fixture/toolpad/application.yml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: application -spec: - authentication: - providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] - authorization: - roles: - - name: admin - description: 'A very important person.' diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml deleted file mode 100644 index e9faea13e80..00000000000 --- a/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/adminpage/page.yml +++ /dev/null @@ -1,26 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: adminpage - display: shell - authorization: - allowAll: false - allowedRoles: - - admin - displayName: Admin Page - content: - - component: Text - name: typography - layout: - columnSize: 1 - props: - value: - $$jsExpression: | - `message: ${hello.data.message}` - queries: - - name: hello - query: - function: hello - kind: local diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml deleted file mode 100644 index 21867f13af7..00000000000 --- a/test/playwright/tmp-0xS4E0/fixture/toolpad/pages/publicpage/page.yml +++ /dev/null @@ -1,15 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: publicpage - display: shell - authorization: - allowAll: true - displayName: Public Page - content: - - component: Text - name: text - props: - value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts deleted file mode 100644 index 7951b807142..00000000000 --- a/test/playwright/tmp-0xS4E0/fixture/toolpad/resources/functions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function hello() { - return { message: 'hello world' }; -} diff --git a/test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore b/test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/playwright/tmp-6n4uK4/fixture/toolpad/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml b/test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml deleted file mode 100644 index 3b44aeea1d8..00000000000 --- a/test/playwright/tmp-6n4uK4/fixture/toolpad/application.yml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: application -spec: - authentication: - providers: [{ provider: google }, { provider: github }] - requiredEmail: - - mui.com diff --git a/test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml b/test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml deleted file mode 100644 index a3fdfc98075..00000000000 --- a/test/playwright/tmp-6n4uK4/fixture/toolpad/pages/mypage/page.yml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - displayName: My Page - title: mypage - display: shell - content: - - component: Text - name: text - props: - value: Hello world diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore b/test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/playwright/tmp-8DcakT/fixture/toolpad/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/application.yml b/test/playwright/tmp-8DcakT/fixture/toolpad/application.yml deleted file mode 100644 index 5cc9fb454cb..00000000000 --- a/test/playwright/tmp-8DcakT/fixture/toolpad/application.yml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: application -spec: - authentication: - providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] - authorization: - roles: - - name: admin - description: 'A very important person.' diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml deleted file mode 100644 index e9faea13e80..00000000000 --- a/test/playwright/tmp-8DcakT/fixture/toolpad/pages/adminpage/page.yml +++ /dev/null @@ -1,26 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: adminpage - display: shell - authorization: - allowAll: false - allowedRoles: - - admin - displayName: Admin Page - content: - - component: Text - name: typography - layout: - columnSize: 1 - props: - value: - $$jsExpression: | - `message: ${hello.data.message}` - queries: - - name: hello - query: - function: hello - kind: local diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml deleted file mode 100644 index 21867f13af7..00000000000 --- a/test/playwright/tmp-8DcakT/fixture/toolpad/pages/publicpage/page.yml +++ /dev/null @@ -1,15 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: publicpage - display: shell - authorization: - allowAll: true - displayName: Public Page - content: - - component: Text - name: text - props: - value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts deleted file mode 100644 index 7951b807142..00000000000 --- a/test/playwright/tmp-8DcakT/fixture/toolpad/resources/functions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function hello() { - return { message: 'hello world' }; -} diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore b/test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/playwright/tmp-FRuzyq/fixture/toolpad/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml b/test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml deleted file mode 100644 index 5cc9fb454cb..00000000000 --- a/test/playwright/tmp-FRuzyq/fixture/toolpad/application.yml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: application -spec: - authentication: - providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] - authorization: - roles: - - name: admin - description: 'A very important person.' diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml deleted file mode 100644 index e9faea13e80..00000000000 --- a/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/adminpage/page.yml +++ /dev/null @@ -1,26 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: adminpage - display: shell - authorization: - allowAll: false - allowedRoles: - - admin - displayName: Admin Page - content: - - component: Text - name: typography - layout: - columnSize: 1 - props: - value: - $$jsExpression: | - `message: ${hello.data.message}` - queries: - - name: hello - query: - function: hello - kind: local diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml deleted file mode 100644 index 21867f13af7..00000000000 --- a/test/playwright/tmp-FRuzyq/fixture/toolpad/pages/publicpage/page.yml +++ /dev/null @@ -1,15 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: publicpage - display: shell - authorization: - allowAll: true - displayName: Public Page - content: - - component: Text - name: text - props: - value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts deleted file mode 100644 index 7951b807142..00000000000 --- a/test/playwright/tmp-FRuzyq/fixture/toolpad/resources/functions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function hello() { - return { message: 'hello world' }; -} diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore b/test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/playwright/tmp-ZLdEe7/fixture/toolpad/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml b/test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml deleted file mode 100644 index 5cc9fb454cb..00000000000 --- a/test/playwright/tmp-ZLdEe7/fixture/toolpad/application.yml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: application -spec: - authentication: - providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] - authorization: - roles: - - name: admin - description: 'A very important person.' diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml deleted file mode 100644 index e9faea13e80..00000000000 --- a/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/adminpage/page.yml +++ /dev/null @@ -1,26 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: adminpage - display: shell - authorization: - allowAll: false - allowedRoles: - - admin - displayName: Admin Page - content: - - component: Text - name: typography - layout: - columnSize: 1 - props: - value: - $$jsExpression: | - `message: ${hello.data.message}` - queries: - - name: hello - query: - function: hello - kind: local diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml deleted file mode 100644 index 21867f13af7..00000000000 --- a/test/playwright/tmp-ZLdEe7/fixture/toolpad/pages/publicpage/page.yml +++ /dev/null @@ -1,15 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: publicpage - display: shell - authorization: - allowAll: true - displayName: Public Page - content: - - component: Text - name: text - props: - value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts deleted file mode 100644 index 7951b807142..00000000000 --- a/test/playwright/tmp-ZLdEe7/fixture/toolpad/resources/functions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function hello() { - return { message: 'hello world' }; -} diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore b/test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/playwright/tmp-z2mqKy/fixture/toolpad/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml b/test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml deleted file mode 100644 index 5cc9fb454cb..00000000000 --- a/test/playwright/tmp-z2mqKy/fixture/toolpad/application.yml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: application -spec: - authentication: - providers: [{ provider: credentials, roles: [{ source: [mock-admin, god], target: admin }] }] - authorization: - roles: - - name: admin - description: 'A very important person.' diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml b/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml deleted file mode 100644 index e9faea13e80..00000000000 --- a/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/adminpage/page.yml +++ /dev/null @@ -1,26 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: adminpage - display: shell - authorization: - allowAll: false - allowedRoles: - - admin - displayName: Admin Page - content: - - component: Text - name: typography - layout: - columnSize: 1 - props: - value: - $$jsExpression: | - `message: ${hello.data.message}` - queries: - - name: hello - query: - function: hello - kind: local diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml b/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml deleted file mode 100644 index 21867f13af7..00000000000 --- a/test/playwright/tmp-z2mqKy/fixture/toolpad/pages/publicpage/page.yml +++ /dev/null @@ -1,15 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.48/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - title: publicpage - display: shell - authorization: - allowAll: true - displayName: Public Page - content: - - component: Text - name: text - props: - value: This page is open to all, regardless of gender, race or social status. diff --git a/test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts b/test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts deleted file mode 100644 index 7951b807142..00000000000 --- a/test/playwright/tmp-z2mqKy/fixture/toolpad/resources/functions.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function hello() { - return { message: 'hello world' }; -} From 3a3d59e90499e68b399dc83c5b49bd2651a97b8d Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:47:41 +0000 Subject: [PATCH 44/58] Remove more unwanted things --- test/visual/components/fixture/.gitignore | 1 - test/visual/components/fixture/pages/page/page.yml | 7 ------- 2 files changed, 8 deletions(-) delete mode 100644 test/visual/components/fixture/.gitignore delete mode 100644 test/visual/components/fixture/pages/page/page.yml diff --git a/test/visual/components/fixture/.gitignore b/test/visual/components/fixture/.gitignore deleted file mode 100644 index 5f1e4d07bfd..00000000000 --- a/test/visual/components/fixture/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.generated diff --git a/test/visual/components/fixture/pages/page/page.yml b/test/visual/components/fixture/pages/page/page.yml deleted file mode 100644 index c0e71daee55..00000000000 --- a/test/visual/components/fixture/pages/page/page.yml +++ /dev/null @@ -1,7 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/mui/mui-toolpad/v0.1.44/docs/schemas/v1/definitions.json#properties/Page - -apiVersion: v1 -kind: page -spec: - id: PLaCtFN - title: Default page From 4445f9d00020f36a282cb1e29ca41bd7b53620c4 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 1 Feb 2024 18:39:29 +0000 Subject: [PATCH 45/58] Update @auth/core, fix error message --- packages/toolpad-app/package.json | 2 +- packages/toolpad-app/src/server/auth.ts | 3 ++- pnpm-lock.yaml | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/toolpad-app/package.json b/packages/toolpad-app/package.json index 772ed362427..15c042dc639 100644 --- a/packages/toolpad-app/package.json +++ b/packages/toolpad-app/package.json @@ -41,7 +41,7 @@ } }, "dependencies": { - "@auth/core": "0.20.0", + "@auth/core": "0.25.0", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.3", "@emotion/server": "11.11.0", diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index a1d1ce9ce6d..c3c9932a85b 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -75,7 +75,7 @@ export function createAuthHandler(project: ToolpadProject): Router { if (!process.env.TOOLPAD_AUTH_SECRET) { console.error( `\n${chalk.red( - 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at https://mui.com/toolpad/concepts/authentication/#authentication-providers', + 'Missing secret for authentication. Please provide a secret in the TOOLPAD_AUTH_SECRET environment variable. Read more at https://mui.com/toolpad/concepts/authentication/#authentication-secret', )}\n`, ); } @@ -188,6 +188,7 @@ export function createAuthHandler(project: ToolpadProject): Router { }); const authConfig: AuthConfig = { + basePath: `${base}/api/auth`, pages: { signIn: `${base}/signin`, signOut: base, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97fae249726..c1eb9e9765d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,8 +484,8 @@ importers: packages/toolpad-app: dependencies: '@auth/core': - specifier: 0.20.0 - version: 0.20.0 + specifier: 0.25.0 + version: 0.25.0 '@emotion/cache': specifier: 11.11.0 version: 11.11.0 @@ -1227,8 +1227,8 @@ packages: engines: {node: '>=16.0.0'} dev: true - /@auth/core@0.20.0: - resolution: {integrity: sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==} + /@auth/core@0.25.0: + resolution: {integrity: sha512-UxENsD+WNlY1NOFsr3Ygc7F1Ypbia8VCd9NU8cbAhSJ0Z8dtyZ0xmssRo1G3ZW50QnW0AaAMuYzY5oumnPMQzA==} peerDependencies: nodemailer: ^6.8.0 peerDependenciesMeta: From b587343d7ecda9dc017c50f9d14d5103b23770dc Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:19:06 +0000 Subject: [PATCH 46/58] Add logged-in user to context --- .../data/toolpad/concepts/custom-functions.md | 13 ++++++++++++ packages/toolpad-app/src/constants.ts | 2 +- .../toolpad-app/src/server/DataManager.ts | 2 +- packages/toolpad-app/src/server/auth.ts | 21 ++----------------- packages/toolpad-app/src/server/rpc.ts | 2 +- packages/toolpad-core/package.json | 2 ++ packages/toolpad-core/src/auth.ts | 20 ++++++++++++++++++ packages/toolpad-core/src/serverRuntime.ts | 20 +++++++++++++++++- packages/toolpad-core/tsconfig.json | 2 +- packages/toolpad-core/typings/@auth.d.ts | 10 +++++++++ packages/toolpad-utils/package.json | 1 + .../src}/httpApiAdapters.ts | 4 ++-- pnpm-lock.yaml | 9 ++++++++ 13 files changed, 82 insertions(+), 26 deletions(-) create mode 100644 packages/toolpad-core/src/auth.ts create mode 100644 packages/toolpad-core/typings/@auth.d.ts rename packages/{toolpad-app/src/server => toolpad-utils/src}/httpApiAdapters.ts (90%) diff --git a/docs/data/toolpad/concepts/custom-functions.md b/docs/data/toolpad/concepts/custom-functions.md index 63e892c9f18..15b9e99e255 100644 --- a/docs/data/toolpad/concepts/custom-functions.md +++ b/docs/data/toolpad/concepts/custom-functions.md @@ -174,3 +174,16 @@ export async function getData() { return api.getData(token); } ``` + +### Get the current authenticated user with `context.user` + +If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated logged-in user, such as their `email`, `name` or `image`. Example: + +```jsx +import { getContext } from '@mui/toolpad/server'; + +export async function getCurrentUserEmail() { + const { user } = getContext(); + return user?.email; +} +``` diff --git a/packages/toolpad-app/src/constants.ts b/packages/toolpad-app/src/constants.ts index 9a8e5503061..5d0b6d21566 100644 --- a/packages/toolpad-app/src/constants.ts +++ b/packages/toolpad-app/src/constants.ts @@ -21,4 +21,4 @@ export const VERSION_CHECK_INTERVAL = 1000 * 60 * 10; // TODO: Remove once global functions UI is ready export const FEATURE_FLAG_GLOBAL_FUNCTIONS = false; -export const FEATURE_FLAG_AUTHORIZATION = false; +export const FEATURE_FLAG_AUTHORIZATION = true; diff --git a/packages/toolpad-app/src/server/DataManager.ts b/packages/toolpad-app/src/server/DataManager.ts index e7462af911c..4c9c4d7c37e 100644 --- a/packages/toolpad-app/src/server/DataManager.ts +++ b/packages/toolpad-app/src/server/DataManager.ts @@ -198,7 +198,7 @@ export default class DataManager { invariant(typeof pageName === 'string', 'pageName url param required'); invariant(typeof queryName === 'string', 'queryName url variable required'); - const ctx = createServerContext(req, res); + const ctx = await createServerContext(req, res); const result = await withContext(ctx, async () => { return this.execQuery(pageName, queryName, req.body); }); diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index c3c9932a85b..b4fe190511f 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -8,9 +8,9 @@ import { AuthConfig, TokenSet } from '@auth/core/types'; import { OAuthConfig } from '@auth/core/providers'; import chalk from 'chalk'; import * as appDom from '@mui/toolpad-core/appDom'; -import { JWT, getToken } from '@auth/core/jwt'; +import { adaptRequestFromExpressToFetch } from '@mui/toolpad-utils/httpApiAdapters'; +import { getUserToken } from '@mui/toolpad-core/auth'; import { asyncHandler } from '../utils/express'; -import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; import type { ToolpadProject } from './localMode'; const SKIP_VERIFICATION_PROVIDERS: appDom.AuthProvider[] = [ @@ -35,23 +35,6 @@ export async function getRequireAuthentication(project: ToolpadProject): Promise return authProviders.length > 0; } -export async function getUserToken(req: express.Request): Promise { - let token = null; - if (process.env.TOOLPAD_AUTH_SECRET) { - const request = adaptRequestFromExpressToFetch(req); - - // @TODO: Library types are wrong as salt should not be required, remove once fixed - // Github discussion: https://github.com/nextauthjs/next-auth/discussions/9133 - // @ts-ignore - token = await getToken({ - req: request, - secret: process.env.TOOLPAD_AUTH_SECRET, - }); - } - - return token; -} - function getMappedRoles( roles: string[], allRoles: string[], diff --git a/packages/toolpad-app/src/server/rpc.ts b/packages/toolpad-app/src/server/rpc.ts index 397c4fa5b3c..8c5b6ddde12 100644 --- a/packages/toolpad-app/src/server/rpc.ts +++ b/packages/toolpad-app/src/server/rpc.ts @@ -74,7 +74,7 @@ export function createRpcHandler(definition: MethodResolvers): express.RequestHa let rawResult; let error: Error | null = null; try { - const ctx = createServerContext(req, res); + const ctx = await createServerContext(req, res); rawResult = await withContext(ctx, async () => { return method({ params, req, res }); }); diff --git a/packages/toolpad-core/package.json b/packages/toolpad-core/package.json index 6e23c420fc3..6ad321f9fd0 100644 --- a/packages/toolpad-core/package.json +++ b/packages/toolpad-core/package.json @@ -41,6 +41,7 @@ "url": "https://github.com/mui/mui-toolpad/issues" }, "dependencies": { + "@auth/core": "0.25.0", "@mui/material": "5.15.6", "@mui/toolpad-utils": "workspace:*", "@tanstack/react-query": "5.17.19", @@ -56,6 +57,7 @@ }, "devDependencies": { "@types/cookie": "0.6.0", + "@types/express": "4.17.21", "@types/invariant": "2.2.37", "@types/react": "18.2.48", "@types/react-is": "18.2.4", diff --git a/packages/toolpad-core/src/auth.ts b/packages/toolpad-core/src/auth.ts new file mode 100644 index 00000000000..b251dfed760 --- /dev/null +++ b/packages/toolpad-core/src/auth.ts @@ -0,0 +1,20 @@ +import type express from 'express'; +import { JWT, getToken } from '@auth/core/jwt'; +import { adaptRequestFromExpressToFetch } from '@mui/toolpad-utils/httpApiAdapters'; + +export async function getUserToken(req: express.Request): Promise { + let token = null; + if (process.env.TOOLPAD_AUTH_SECRET) { + const request = adaptRequestFromExpressToFetch(req); + + // @TODO: Library types are wrong as salt should not be required, remove once fixed + // Github discussion: https://github.com/nextauthjs/next-auth/discussions/9133 + // @ts-ignore + token = await getToken({ + req: request, + secret: process.env.TOOLPAD_AUTH_SECRET, + }); + } + + return token; +} diff --git a/packages/toolpad-core/src/serverRuntime.ts b/packages/toolpad-core/src/serverRuntime.ts index ac77998f0fe..ca2b0ee5a34 100644 --- a/packages/toolpad-core/src/serverRuntime.ts +++ b/packages/toolpad-core/src/serverRuntime.ts @@ -2,6 +2,9 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import { IncomingMessage, ServerResponse } from 'node:http'; import * as cookie from 'cookie'; import { isWebContainer } from '@webcontainer/env'; +import { User } from '@auth/core/types'; +import type express from 'express'; +import { getUserToken } from './auth'; export interface ServerContext { /** @@ -12,6 +15,7 @@ export interface ServerContext { * Use to set a cookie `name` with `value`. */ setCookie: (name: string, value: string) => void; + user: User | null; } const contextStore = new AsyncLocalStorage(); @@ -20,13 +24,27 @@ export function getServerContext(): ServerContext | undefined { return contextStore.getStore(); } -export function createServerContext(req: IncomingMessage, res: ServerResponse): ServerContext { +export async function createServerContext( + req: IncomingMessage, + res: ServerResponse, +): Promise { const cookies = cookie.parse(req.headers.cookie || ''); + + const token = await getUserToken(req as express.Request); + const user = token && { + id: token.sub, + name: token.name, + email: token.email, + image: token.picture, + roles: token.roles, + }; + return { cookies, setCookie(name, value) { res.setHeader('Set-Cookie', cookie.serialize(name, value, { path: '/' })); }, + user, }; } diff --git a/packages/toolpad-core/tsconfig.json b/packages/toolpad-core/tsconfig.json index c27c70b76e2..b3c1c19f646 100644 --- a/packages/toolpad-core/tsconfig.json +++ b/packages/toolpad-core/tsconfig.json @@ -12,5 +12,5 @@ "pretty": true, "preserveWatchOutput": true }, - "include": ["src/**/*.ts", "src/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx", "typings/**/*.d.ts"] } diff --git a/packages/toolpad-core/typings/@auth.d.ts b/packages/toolpad-core/typings/@auth.d.ts new file mode 100644 index 00000000000..2dd16da613d --- /dev/null +++ b/packages/toolpad-core/typings/@auth.d.ts @@ -0,0 +1,10 @@ +export declare module '@auth/core/types' { + interface User { + roles: string[]; + } +} +export declare module '@auth/core/jwt' { + interface JWT { + roles: string[]; + } +} diff --git a/packages/toolpad-utils/package.json b/packages/toolpad-utils/package.json index 3731275497d..caf6ddfd10c 100644 --- a/packages/toolpad-utils/package.json +++ b/packages/toolpad-utils/package.json @@ -60,6 +60,7 @@ "yaml-diff-patch": "2.0.0" }, "devDependencies": { + "@types/express": "4.17.21", "@types/invariant": "2.2.37", "@types/prettier": "2.7.3", "@types/react": "18.2.48", diff --git a/packages/toolpad-app/src/server/httpApiAdapters.ts b/packages/toolpad-utils/src/httpApiAdapters.ts similarity index 90% rename from packages/toolpad-app/src/server/httpApiAdapters.ts rename to packages/toolpad-utils/src/httpApiAdapters.ts index 84007964088..c87c3d869db 100644 --- a/packages/toolpad-app/src/server/httpApiAdapters.ts +++ b/packages/toolpad-utils/src/httpApiAdapters.ts @@ -1,10 +1,10 @@ -import express from 'express'; +import type express from 'express'; export function encodeRequestBody(req: express.Request) { const contentType = req.headers['content-type']; if (typeof req.body === 'object' && contentType?.includes('application/x-www-form-urlencoded')) { - return Object.entries(req.body as Record).reduce((acc, [key, value]) => { + return Object.entries(req.body as Record).reduce((acc, [key, value]) => { const encKey = encodeURIComponent(key); const encValue = encodeURIComponent(value); return `${acc ? `${acc}&` : ''}${encKey}=${encValue}`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1eb9e9765d..579c91fb96f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -899,6 +899,9 @@ importers: packages/toolpad-core: dependencies: + '@auth/core': + specifier: 0.25.0 + version: 0.25.0 '@mui/material': specifier: 5.15.6 version: 5.15.6(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) @@ -945,6 +948,9 @@ importers: '@types/cookie': specifier: 0.6.0 version: 0.6.0 + '@types/express': + specifier: 4.17.21 + version: 4.17.21 '@types/invariant': specifier: 2.2.37 version: 2.2.37 @@ -982,6 +988,9 @@ importers: specifier: 2.0.0 version: 2.0.0 devDependencies: + '@types/express': + specifier: 4.17.21 + version: 4.17.21 '@types/invariant': specifier: 2.2.37 version: 2.2.37 From 6dae561bd15d3268a5c4cf4185f10dee0013c249 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:25:34 +0000 Subject: [PATCH 47/58] Fix type --- packages/toolpad-utils/src/httpApiAdapters.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/toolpad-utils/src/httpApiAdapters.ts b/packages/toolpad-utils/src/httpApiAdapters.ts index c87c3d869db..72f1cc30c34 100644 --- a/packages/toolpad-utils/src/httpApiAdapters.ts +++ b/packages/toolpad-utils/src/httpApiAdapters.ts @@ -4,11 +4,14 @@ export function encodeRequestBody(req: express.Request) { const contentType = req.headers['content-type']; if (typeof req.body === 'object' && contentType?.includes('application/x-www-form-urlencoded')) { - return Object.entries(req.body as Record).reduce((acc, [key, value]) => { - const encKey = encodeURIComponent(key); - const encValue = encodeURIComponent(value); - return `${acc ? `${acc}&` : ''}${encKey}=${encValue}`; - }, ''); + return Object.entries(req.body as Record).reduce( + (acc, [key, value]) => { + const encKey = encodeURIComponent(key); + const encValue = encodeURIComponent(value); + return `${acc ? `${acc}&` : ''}${encKey}=${encValue}`; + }, + '', + ); } if (contentType?.includes('application/json')) { From 3ebe46a6e3c6c93b88af73af2bb157a438db2817 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:28:55 +0000 Subject: [PATCH 48/58] Remove feature flag changes --- packages/toolpad-app/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolpad-app/src/constants.ts b/packages/toolpad-app/src/constants.ts index 5d0b6d21566..9a8e5503061 100644 --- a/packages/toolpad-app/src/constants.ts +++ b/packages/toolpad-app/src/constants.ts @@ -21,4 +21,4 @@ export const VERSION_CHECK_INTERVAL = 1000 * 60 * 10; // TODO: Remove once global functions UI is ready export const FEATURE_FLAG_GLOBAL_FUNCTIONS = false; -export const FEATURE_FLAG_AUTHORIZATION = true; +export const FEATURE_FLAG_AUTHORIZATION = false; From f2d4a6560127c82660b1b28ca8c4fcfcf8b90a51 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:53:31 +0000 Subject: [PATCH 49/58] Update docs more, change API a bit --- docs/data/toolpad/concepts/custom-functions.md | 2 +- docs/data/toolpad/reference/api/get-context.md | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/data/toolpad/concepts/custom-functions.md b/docs/data/toolpad/concepts/custom-functions.md index 15b9e99e255..98c8219f773 100644 --- a/docs/data/toolpad/concepts/custom-functions.md +++ b/docs/data/toolpad/concepts/custom-functions.md @@ -177,7 +177,7 @@ export async function getData() { ### Get the current authenticated user with `context.user` -If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated logged-in user, such as their `email`, `name` or `image`. Example: +If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated logged-in user, such as their `email`, `name` or `avatar`. Example: ```jsx import { getContext } from '@mui/toolpad/server'; diff --git a/docs/data/toolpad/reference/api/get-context.md b/docs/data/toolpad/reference/api/get-context.md index 19dfcd53bd3..abbe8e046bc 100644 --- a/docs/data/toolpad/reference/api/get-context.md +++ b/docs/data/toolpad/reference/api/get-context.md @@ -33,14 +33,15 @@ a `ServerContext` containing information on the context the backend function was ### ServerContext -This described a certain context under which a backend function was called. +This describes a certain context under which a backend function was called. **Properties** -| Name | Type | Description | -| :---------- | :-------------------------------------- | :------------------------------------------------ | -| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | -| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | +| Name | Type | Description | +| :---------- | :----------------------------------------------------------------- | :------------------------------------------------------------------------ | +| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | +| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | +| `user` | `{ name: string; email: string; avatar: string; roles: string[] }` | Get current [authenticated](/toolpad/concepts/authentication/) user data. | ## Usage From 3e786c9e15074b9f7b1ee8d79cd55d02fdb5914c Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:54:19 +0000 Subject: [PATCH 50/58] Non-docs changes --- packages/toolpad-core/src/serverRuntime.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/toolpad-core/src/serverRuntime.ts b/packages/toolpad-core/src/serverRuntime.ts index ca2b0ee5a34..8020f6ee50a 100644 --- a/packages/toolpad-core/src/serverRuntime.ts +++ b/packages/toolpad-core/src/serverRuntime.ts @@ -2,10 +2,16 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import { IncomingMessage, ServerResponse } from 'node:http'; import * as cookie from 'cookie'; import { isWebContainer } from '@webcontainer/env'; -import { User } from '@auth/core/types'; import type express from 'express'; import { getUserToken } from './auth'; +interface ContextUser { + name: string; + email: string; + avatar: string; + roles: string[]; +} + export interface ServerContext { /** * A dictionary mapping cookie name to cookie value. @@ -15,7 +21,7 @@ export interface ServerContext { * Use to set a cookie `name` with `value`. */ setCookie: (name: string, value: string) => void; - user: User | null; + user: ContextUser | null; } const contextStore = new AsyncLocalStorage(); @@ -32,10 +38,9 @@ export async function createServerContext( const token = await getUserToken(req as express.Request); const user = token && { - id: token.sub, name: token.name, email: token.email, - image: token.picture, + avatar: token.picture, roles: token.roles, }; From 935fd36097f4674d8e36cba59792c98701199476 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 8 Feb 2024 18:56:44 +0000 Subject: [PATCH 51/58] Better type name --- packages/toolpad-core/src/serverRuntime.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolpad-core/src/serverRuntime.ts b/packages/toolpad-core/src/serverRuntime.ts index 8020f6ee50a..d8a3affebe1 100644 --- a/packages/toolpad-core/src/serverRuntime.ts +++ b/packages/toolpad-core/src/serverRuntime.ts @@ -5,7 +5,7 @@ import { isWebContainer } from '@webcontainer/env'; import type express from 'express'; import { getUserToken } from './auth'; -interface ContextUser { +interface ServerContextUser { name: string; email: string; avatar: string; @@ -21,7 +21,7 @@ export interface ServerContext { * Use to set a cookie `name` with `value`. */ setCookie: (name: string, value: string) => void; - user: ContextUser | null; + user: ServerContextUser | null; } const contextStore = new AsyncLocalStorage(); From b852709845ac41edc101d6144c8f1fcc1e359547 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 8 Feb 2024 19:05:44 +0000 Subject: [PATCH 52/58] Fix types --- packages/toolpad-core/src/serverRuntime.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/toolpad-core/src/serverRuntime.ts b/packages/toolpad-core/src/serverRuntime.ts index d8a3affebe1..4381b2c384c 100644 --- a/packages/toolpad-core/src/serverRuntime.ts +++ b/packages/toolpad-core/src/serverRuntime.ts @@ -6,10 +6,10 @@ import type express from 'express'; import { getUserToken } from './auth'; interface ServerContextUser { - name: string; - email: string; - avatar: string; - roles: string[]; + name?: string | null; + email?: string | null; + avatar?: string | null; + roles?: string[]; } export interface ServerContext { From e35837fb3aa777cf37e59c7d228d5aca36e49f7a Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 8 Feb 2024 19:13:16 +0000 Subject: [PATCH 53/58] Adjust copy --- docs/data/toolpad/reference/api/get-context.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/data/toolpad/reference/api/get-context.md b/docs/data/toolpad/reference/api/get-context.md index abbe8e046bc..f1cb9e5c501 100644 --- a/docs/data/toolpad/reference/api/get-context.md +++ b/docs/data/toolpad/reference/api/get-context.md @@ -37,11 +37,11 @@ This describes a certain context under which a backend function was called. **Properties** -| Name | Type | Description | -| :---------- | :----------------------------------------------------------------- | :------------------------------------------------------------------------ | -| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | -| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | -| `user` | `{ name: string; email: string; avatar: string; roles: string[] }` | Get current [authenticated](/toolpad/concepts/authentication/) user data. | +| Name | Type | Description | +| :---------- | :----------------------------------------------------------------- | :---------------------------------------------------------------------------------- | +| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | +| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | +| `user` | `{ name: string; email: string; avatar: string; roles: string[] }` | Get current [authenticated](/toolpad/concepts/authentication/) logged-in user data. | ## Usage From 172d2c385c363a1c959d3ffb219dc42d8027257c Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:40:50 +0000 Subject: [PATCH 54/58] Use react router, fix Firefox test --- packages/toolpad-app/src/runtime/ToolpadApp.tsx | 2 +- packages/toolpad-app/src/runtime/useAuth.ts | 12 ++++++++---- test/integration/auth/domain.spec.ts | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index 06ac428fa52..8b5ffdb4b2b 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -1660,7 +1660,7 @@ export function ToolpadAppProvider({ (window as any).toggleDevtools = () => toggleDevtools(); }, [toggleDevtools]); - const authContext = useAuth({ dom, basename, signInPagePath: `${basename}/signin` }); + const authContext = useAuth({ dom, basename, signInPagePath: '/signin' }); const appHost = useNonNullableContext(AppHostContext); const showPreviewHeader = shouldShowPreviewHeader(appHost); diff --git a/packages/toolpad-app/src/runtime/useAuth.ts b/packages/toolpad-app/src/runtime/useAuth.ts index dc7dcb87abc..6d5469152ee 100644 --- a/packages/toolpad-app/src/runtime/useAuth.ts +++ b/packages/toolpad-app/src/runtime/useAuth.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import * as appDom from '@mui/toolpad-core/appDom'; import { useNonNullableContext } from '@mui/toolpad-utils/react'; +import { useLocation, useNavigate } from 'react-router-dom'; import { AppHostContext } from './AppHostContext'; const AUTH_API_PATH = '/api/auth'; @@ -52,10 +53,13 @@ export const AuthContext = React.createContext({ interface UseAuthInput { dom: appDom.RenderTree; basename: string; - signInPagePath?: string; + signInPagePath: string; } export function useAuth({ dom, basename, signInPagePath }: UseAuthInput): AuthPayload { + const location = useLocation(); + const navigate = useNavigate(); + const authProviders = React.useMemo(() => { const app = appDom.getApp(dom); const authProviderConfigs = app.attributes.authentication?.providers ?? []; @@ -107,10 +111,10 @@ export function useAuth({ dom, basename, signInPagePath }: UseAuthInput): AuthPa setSession(null); setIsSigningOut(false); - if (!signInPagePath || window.location.pathname !== signInPagePath) { - window.location.href = `${basename}${AUTH_SIGNIN_PATH}`; + if (location.pathname !== signInPagePath) { + navigate(signInPagePath); } - }, [basename, getCsrfToken, signInPagePath]); + }, [basename, getCsrfToken, location.pathname, navigate, signInPagePath]); const getSession = React.useCallback(async () => { setIsSigningIn(true); diff --git a/test/integration/auth/domain.spec.ts b/test/integration/auth/domain.spec.ts index 22a3d1d97b5..d5462762f23 100644 --- a/test/integration/auth/domain.spec.ts +++ b/test/integration/auth/domain.spec.ts @@ -8,6 +8,8 @@ const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); test.use({ ignoreConsoleErrors: [ /Failed to load resource: the server responded with a status of 401 \(Unauthorized\)/, + /NetworkError when attempting to fetch resource./, + /The operation was aborted./, ], }); From 80a5a6010240aa9d268e109c18ad135bdce8a70e Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:42:14 +0000 Subject: [PATCH 55/58] Show ServerContextSession in separate table in docs, rename user to session in context --- .../data/toolpad/concepts/custom-functions.md | 8 +++---- .../data/toolpad/reference/api/get-context.md | 21 ++++++++++++++----- packages/toolpad-core/src/serverRuntime.ts | 11 ++++++---- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/data/toolpad/concepts/custom-functions.md b/docs/data/toolpad/concepts/custom-functions.md index 98c8219f773..1bf924e2368 100644 --- a/docs/data/toolpad/concepts/custom-functions.md +++ b/docs/data/toolpad/concepts/custom-functions.md @@ -175,15 +175,15 @@ export async function getData() { } ``` -### Get the current authenticated user with `context.user` +### Get the current authenticated user session with `context.session` -If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated logged-in user, such as their `email`, `name` or `avatar`. Example: +If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated logged-in user session, such as the user's `email`, `name` or `avatar`. Example: ```jsx import { getContext } from '@mui/toolpad/server'; export async function getCurrentUserEmail() { - const { user } = getContext(); - return user?.email; + const { session } = getContext(); + return session?.email; } ``` diff --git a/docs/data/toolpad/reference/api/get-context.md b/docs/data/toolpad/reference/api/get-context.md index f1cb9e5c501..ee355536848 100644 --- a/docs/data/toolpad/reference/api/get-context.md +++ b/docs/data/toolpad/reference/api/get-context.md @@ -37,11 +37,22 @@ This describes a certain context under which a backend function was called. **Properties** -| Name | Type | Description | -| :---------- | :----------------------------------------------------------------- | :---------------------------------------------------------------------------------- | -| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | -| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | -| `user` | `{ name: string; email: string; avatar: string; roles: string[] }` | Get current [authenticated](/toolpad/concepts/authentication/) logged-in user data. | +| Name | Type | Description | +| :---------- | :-------------------------------------- | :------------------------------------------------------------------------------------------ | +| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | +| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | +| `session` | `ServerContextSession` | Get current [authenticated](/toolpad/concepts/authentication/) logged-in user session data. | + +### ServerContextSession + +**Properties** + +| Name | Type | Description | +| :-------- | :--------------- | :------------------------------- | +| `name?` | `string \| null` | Logged-in user name. | +| `email?` | `string \| null` | Logged-in user email. | +| `avatar?` | `string \| null` | Logged-in user avatar image URL. | +| `roles` | `string[]` | Logged-in user roles in Toolpad. | ## Usage diff --git a/packages/toolpad-core/src/serverRuntime.ts b/packages/toolpad-core/src/serverRuntime.ts index 4381b2c384c..724f678aa12 100644 --- a/packages/toolpad-core/src/serverRuntime.ts +++ b/packages/toolpad-core/src/serverRuntime.ts @@ -5,7 +5,7 @@ import { isWebContainer } from '@webcontainer/env'; import type express from 'express'; import { getUserToken } from './auth'; -interface ServerContextUser { +interface ServerContextSession { name?: string | null; email?: string | null; avatar?: string | null; @@ -21,7 +21,10 @@ export interface ServerContext { * Use to set a cookie `name` with `value`. */ setCookie: (name: string, value: string) => void; - user: ServerContextUser | null; + /** + * Data about current logged-in user session, if authenticated. + */ + session: ServerContextSession | null; } const contextStore = new AsyncLocalStorage(); @@ -37,7 +40,7 @@ export async function createServerContext( const cookies = cookie.parse(req.headers.cookie || ''); const token = await getUserToken(req as express.Request); - const user = token && { + const session = token && { name: token.name, email: token.email, avatar: token.picture, @@ -49,7 +52,7 @@ export async function createServerContext( setCookie(name, value) { res.setHeader('Set-Cookie', cookie.serialize(name, value, { path: '/' })); }, - user, + session, }; } From 696801809d3b2b260c827c5493a204f622179b5a Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:55:39 +0000 Subject: [PATCH 56/58] Include user inside session instead --- docs/data/toolpad/concepts/custom-functions.md | 4 ++-- docs/data/toolpad/reference/api/get-context.md | 12 ++++++------ packages/toolpad-core/src/serverRuntime.ts | 18 ++++++++++++------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/data/toolpad/concepts/custom-functions.md b/docs/data/toolpad/concepts/custom-functions.md index 1bf924e2368..28d19a5d5cb 100644 --- a/docs/data/toolpad/concepts/custom-functions.md +++ b/docs/data/toolpad/concepts/custom-functions.md @@ -177,13 +177,13 @@ export async function getData() { ### Get the current authenticated user session with `context.session` -If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated logged-in user session, such as the user's `email`, `name` or `avatar`. Example: +If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated session, such as the logged-in user's `email`, `name` or `avatar`. Example: ```jsx import { getContext } from '@mui/toolpad/server'; export async function getCurrentUserEmail() { const { session } = getContext(); - return session?.email; + return session?.user.email; } ``` diff --git a/docs/data/toolpad/reference/api/get-context.md b/docs/data/toolpad/reference/api/get-context.md index ee355536848..d1ffa2a00cd 100644 --- a/docs/data/toolpad/reference/api/get-context.md +++ b/docs/data/toolpad/reference/api/get-context.md @@ -37,13 +37,13 @@ This describes a certain context under which a backend function was called. **Properties** -| Name | Type | Description | -| :---------- | :-------------------------------------- | :------------------------------------------------------------------------------------------ | -| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | -| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | -| `session` | `ServerContextSession` | Get current [authenticated](/toolpad/concepts/authentication/) logged-in user session data. | +| Name | Type | Description | +| :---------- | :-------------------------------------- | :--------------------------------------------------------------------------- | +| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | +| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | +| `session` | `{ user: ServerContextSessionUser }` | Get current [authenticated](/toolpad/concepts/authentication/) session data. | -### ServerContextSession +### ServerContextSessionUser **Properties** diff --git a/packages/toolpad-core/src/serverRuntime.ts b/packages/toolpad-core/src/serverRuntime.ts index 724f678aa12..7f667b37271 100644 --- a/packages/toolpad-core/src/serverRuntime.ts +++ b/packages/toolpad-core/src/serverRuntime.ts @@ -5,13 +5,17 @@ import { isWebContainer } from '@webcontainer/env'; import type express from 'express'; import { getUserToken } from './auth'; -interface ServerContextSession { +interface ServerContextSessionUser { name?: string | null; email?: string | null; avatar?: string | null; roles?: string[]; } +interface ServerContextSession { + user: ServerContextSessionUser; +} + export interface ServerContext { /** * A dictionary mapping cookie name to cookie value. @@ -22,7 +26,7 @@ export interface ServerContext { */ setCookie: (name: string, value: string) => void; /** - * Data about current logged-in user session, if authenticated. + * Data about current authenticated session. */ session: ServerContextSession | null; } @@ -41,10 +45,12 @@ export async function createServerContext( const token = await getUserToken(req as express.Request); const session = token && { - name: token.name, - email: token.email, - avatar: token.picture, - roles: token.roles, + user: { + name: token.name, + email: token.email, + avatar: token.picture, + roles: token.roles, + }, }; return { From 937e9307d8e7da481454c1ef3655ab916ce70961 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:59:45 +0000 Subject: [PATCH 57/58] Adjustments --- .../data/toolpad/concepts/custom-functions.md | 2 +- .../data/toolpad/reference/api/get-context.md | 22 +++++++++---------- packages/toolpad-core/src/serverRuntime.ts | 6 +---- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/data/toolpad/concepts/custom-functions.md b/docs/data/toolpad/concepts/custom-functions.md index 28d19a5d5cb..06ec53a778f 100644 --- a/docs/data/toolpad/concepts/custom-functions.md +++ b/docs/data/toolpad/concepts/custom-functions.md @@ -175,7 +175,7 @@ export async function getData() { } ``` -### Get the current authenticated user session with `context.session` +### Get the current authenticated session with `context.session` If your Toolpad app has [authentication](/toolpad/concepts/authentication/) enabled, you can get data from the authenticated session, such as the logged-in user's `email`, `name` or `avatar`. Example: diff --git a/docs/data/toolpad/reference/api/get-context.md b/docs/data/toolpad/reference/api/get-context.md index d1ffa2a00cd..87ef42af696 100644 --- a/docs/data/toolpad/reference/api/get-context.md +++ b/docs/data/toolpad/reference/api/get-context.md @@ -37,22 +37,22 @@ This describes a certain context under which a backend function was called. **Properties** -| Name | Type | Description | -| :---------- | :-------------------------------------- | :--------------------------------------------------------------------------- | -| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | -| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | -| `session` | `{ user: ServerContextSessionUser }` | Get current [authenticated](/toolpad/concepts/authentication/) session data. | +| Name | Type | Description | +| :---------- | :------------------------------------------- | :--------------------------------------------------------------------------- | +| `cookies` | `Record` | A dictionary mapping cookie name to cookie value. | +| `setCookie` | `(name: string, value: string) => void` | Use to set a cookie `name` with `value`. | +| `session` | `{ user: ServerContextSessionUser } \| null` | Get current [authenticated](/toolpad/concepts/authentication/) session data. | ### ServerContextSessionUser **Properties** -| Name | Type | Description | -| :-------- | :--------------- | :------------------------------- | -| `name?` | `string \| null` | Logged-in user name. | -| `email?` | `string \| null` | Logged-in user email. | -| `avatar?` | `string \| null` | Logged-in user avatar image URL. | -| `roles` | `string[]` | Logged-in user roles in Toolpad. | +| Name | Type | Description | +| :-------- | :--------------- | :---------------------------------------------------------- | +| `name?` | `string \| null` | Logged-in user name. | +| `email?` | `string \| null` | Logged-in user email. | +| `avatar?` | `string \| null` | Logged-in user avatar image URL. | +| `roles` | `string[]` | Logged-in user [roles](/toolpad/concepts/rbac/) in Toolpad. | ## Usage diff --git a/packages/toolpad-core/src/serverRuntime.ts b/packages/toolpad-core/src/serverRuntime.ts index 7f667b37271..d9424c4ad2e 100644 --- a/packages/toolpad-core/src/serverRuntime.ts +++ b/packages/toolpad-core/src/serverRuntime.ts @@ -12,10 +12,6 @@ interface ServerContextSessionUser { roles?: string[]; } -interface ServerContextSession { - user: ServerContextSessionUser; -} - export interface ServerContext { /** * A dictionary mapping cookie name to cookie value. @@ -28,7 +24,7 @@ export interface ServerContext { /** * Data about current authenticated session. */ - session: ServerContextSession | null; + session: { user: ServerContextSessionUser } | null; } const contextStore = new AsyncLocalStorage(); From 58141ef160077d9bbf7a0dca4ef2abee759b503c Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:14:52 +0000 Subject: [PATCH 58/58] Cover session in context in tests --- test/integration/auth/domain.spec.ts | 2 +- .../auth/fixture-domain/toolpad/pages/mypage/page.yml | 6 +++--- .../auth/fixture-domain/toolpad/resources/functions.ts | 7 +++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/test/integration/auth/domain.spec.ts b/test/integration/auth/domain.spec.ts index d5462762f23..1de9c69d3d4 100644 --- a/test/integration/auth/domain.spec.ts +++ b/test/integration/auth/domain.spec.ts @@ -43,7 +43,7 @@ test('Must be authenticated with valid domain to access app', async ({ page, req // Sign in with valid domain await tryCredentialsSignIn(page, 'mui', 'mui'); await expect(page).toHaveURL(/\/prod\/pages\/mypage/); - await expect(page.getByText('message: hello world')).toBeVisible(); + await expect(page.getByText('my email: test@mui.com')).toBeVisible(); // Is not redirected when authenticated await page.goto('/prod/pages/mypage'); diff --git a/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml b/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml index 31ebc4c0340..d58fd663f57 100644 --- a/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml +++ b/test/integration/auth/fixture-domain/toolpad/pages/mypage/page.yml @@ -12,9 +12,9 @@ spec: props: value: $$jsExpression: | - `message: ${hello.data.message}` + `my email: ${getMySession.data.user.email}` queries: - - name: hello + - name: getMySession query: - function: hello + function: getMySession kind: local diff --git a/test/integration/auth/fixture-domain/toolpad/resources/functions.ts b/test/integration/auth/fixture-domain/toolpad/resources/functions.ts index 7951b807142..3a54984c879 100644 --- a/test/integration/auth/fixture-domain/toolpad/resources/functions.ts +++ b/test/integration/auth/fixture-domain/toolpad/resources/functions.ts @@ -1,3 +1,6 @@ -export async function hello() { - return { message: 'hello world' }; +import { getContext } from '@mui/toolpad/server'; + +export async function getMySession() { + const context = getContext(); + return context.session; }