Skip to content

Commit

Permalink
(re)Add support for publicPage redirects using pageMiddleware (#8280)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>
  • Loading branch information
dcousens and dcousens committed Jan 30, 2023
1 parent 48ff6f1 commit 384748d
Show file tree
Hide file tree
Showing 21 changed files with 212 additions and 303 deletions.
3 changes: 1 addition & 2 deletions .changeset/five-papayas-mate.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
'@keystone-6/core': minor
'@keystone-6/website': minor
---

Add acl option for s3 storage configuration.
Add `acl` option for s3 storage configuration
6 changes: 6 additions & 0 deletions .changeset/stones-are-fallen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@keystone-6/auth': major
'@keystone-6/core': major
---

Renames `isValidSession` on `pageMiddleware` to `wasAccessAllowed`, which is what it actually is
6 changes: 6 additions & 0 deletions .changeset/stones-are-falling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@keystone-6/auth': patch
'@keystone-6/core': patch
---

Fixes `pageMiddleware` to be called for `publicPages`, returning support for public redirects
10 changes: 6 additions & 4 deletions docs/pages/docs/config/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,13 @@ Fine grained configuration of how lists and fields behave in the Admin UI is han
Options:

- `isDisabled` (default: `false`): If `isDisabled` is set to `true` then the Admin UI will be completely disabled.
- `isAccessAllowed` (default: `(context) => !!context.session`): This function controls whether a user is able to access the Admin UI.
- `isAccessAllowed` (default: `(context) => context.session !== undefined`): This function controls whether a user can view the Admin UI.
It takes a [`KeystoneContext`](../context/overview) object as an argument.

Advanced configuration:

- `publicPages` (default: `[]`): An array of page routes that can be accessed without passing the `isAccessAllowed` check.
- `publicPages` (default: `[]`): An array of page routes that bypass the `isAccessAllowed` function.
- `pageMiddleware` (default: `undefined`): An async middleware function that can optionally return a redirect
- `getAdditionalFiles` (default: `[]`): An async function returns an array of `AdminFileToWrite` objects indicating files to be added to the system at `build` time.
If the `mode` is `'write'`, then the code to be written to the file should be provided as the `src` argument.
If the `mode` is `'copy'` then an `inputPath` value should be provided.
Expand All @@ -166,8 +167,9 @@ Advanced configuration:
export default config({
ui: {
isDisabled: false,
isAccessAllowed: async context => true,
// Optional advanced configuration
isAccessAllowed: async (context) => context.session !== undefined,

// advanced configuration
publicPages: ['/welcome'],
getAdditionalFiles: [
async (config: KeystoneConfig) => [
Expand Down
30 changes: 19 additions & 11 deletions examples/auth/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,42 @@ import { lists } from './schema';
* packages/auth/src/getExtendGraphQLSchema.ts
*/

let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --';
let sessionMaxAge = 60 * 60 * 24 * 30; // 30 days
const sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --';
const sessionMaxAge = 60 * 60 * 24 * 30; // 30 days

// createAuth configures signin functionality based on the config below. Note this only implements
// authentication, i.e signing in as an item using identity and secret fields in a list. Session
// management and access control are controlled independently in the main keystone config.
const { withAuth } = createAuth({
// This is the list that contains items people can sign in as
listKey: 'User',

// The identity field is typically a username or email address
identityField: 'email',

// The secret field must be a password type field
secretField: 'password',

/* TODO -- review this later, it's not implemented yet and not fully designed (e.g error cases)
// This ensures than an item is actually able to sign in
validateItem: ({ item }) => item.isEnabled,
*/

// initFirstItem turns on the "First User" experience, which prompts you to create a new user
// when there are no items in the list yet
// when there are no items in the list
initFirstItem: {
// These fields are collected in the "Create First User" form
fields: ['name', 'email', 'password'],
// This is additional data that will be set when creating the first item
// This is additional data that will always be set for the first item
itemData: {
// We need to specify that isAdmin is true for the first item, so the user can access the
// Admin UI (see isAccessAllowed in the admin config below)
// isAdmin is true, so the admin can pass isAccessAllowed (see below)
isAdmin: true,
// Only enabled users can sign in, so we need to set this as well
// TODO: Come back to this when we review how to restrict signin to valid users
// isEnabled: true,
},
},
// Populate session.data based on the authed user
sessionData: 'name isAdmin',

// add isAdmin for the authed user (required by isAccessAllowed)
sessionData: 'isAdmin',

/* TODO -- complete the UI for these features and enable them
passwordResetLink: {
sendToken(args) {
Expand Down Expand Up @@ -72,5 +74,11 @@ export default withAuth(
// The session secret is used to encrypt cookie data (should be an environment variable)
secret: sessionSecret,
}),
ui: {
// only admins can view the AdminUI
isAccessAllowed: ({ session }) => {
return session?.data?.isAdmin;
},
},
})
);
21 changes: 3 additions & 18 deletions examples/auth/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { list } from '@keystone-6/core';
import { allOperations, allowAll } from '@keystone-6/core/access';
import { text, checkbox, password } from '@keystone-6/core/fields';
import type { Lists } from '.keystone/types';

export const lists = {
export const lists: Lists = {
User: list({
access: {
operation: {
Expand Down Expand Up @@ -43,7 +44,7 @@ export const lists = {
session && (session.data.isAdmin || session.itemId === item.id) ? 'edit' : 'hidden',
},
listView: {
fieldMode: ({ session }) => (session?.item?.isAdmin ? 'read' : 'hidden'),
fieldMode: ({ session }) => (session?.data?.isAdmin ? 'read' : 'hidden'),
},
},
}),
Expand All @@ -64,22 +65,6 @@ export const lists = {
},
},
}),
/* TODO: Come back to this when we review how to restrict signin to valid users
// This controls whether users can sign in or not
isEnabled: checkbox({
access: {
// Only Admins can change the isEnabled flag for any users
// create: ({ session }) => session?.data.isAdmin,
update: ({ session }) => session?.data.isAdmin,
},
ui: {
// All users can see the isEnabled status, only admins can change it
itemView: {
fieldMode: ({ session }) => (session?.data.isAdmin ? 'edit' : 'read'),
},
},
}),
*/
},
}),
};
14 changes: 0 additions & 14 deletions examples/basic/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,12 @@ const auth = createAuth({
sessionData: 'name isAdmin',
});

// TODO -- Create a separate example for access control in the Admin UI
// const isAccessAllowed = ({ session }: { session: any }) => !!session?.item?.isAdmin;

export default auth.withAuth(
config({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
},
ui: {
// NOTE -- this is not implemented, keystone currently always provides an admin ui at /
// path: '/admin',
// isAccessAllowed,
},
storage: {
my_images: {
kind: 'local',
Expand All @@ -57,11 +49,5 @@ export default auth.withAuth(
lists,
extendGraphqlSchema,
session: statelessSessions({ maxAge: sessionMaxAge, secret: sessionSecret }),
// TODO -- Create a separate example for stored/redis sessions
// session: storedSessions({
// store: new Map(),
// // store: redisSessionStore({ client: redis.createClient() }),
// secret: sessionSecret,
// }),
})
);
4 changes: 0 additions & 4 deletions examples/ecommerce/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ export default withAuth(
Role,
},
extendGraphqlSchema,
ui: {
// Show the UI only for poeple who pass this test
isAccessAllowed: ({ session }) => !!session,
},
session: statelessSessions(sessionConfig),
})
);
1 change: 1 addition & 0 deletions examples/roles/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const permissions = {
canManageAllTodos: ({ session }: ListAccessArgs) => !!session?.data.role?.canManageAllTodos,
canManagePeople: ({ session }: ListAccessArgs) => !!session?.data.role?.canManagePeople,
canManageRoles: ({ session }: ListAccessArgs) => !!session?.data.role?.canManageRoles,
// TODO: add canViewAdminUI
};

/*
Expand Down
3 changes: 3 additions & 0 deletions examples/roles/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export default withAuth(
},
lists,
ui: {
// TODO: isSignedIn is the default, a better example would be limiting users
// isAccessAllowed: canViewAdminUI,

/* Everyone who is signed in can access the Admin UI */
isAccessAllowed: isSignedIn,
},
Expand Down
57 changes: 0 additions & 57 deletions packages/auth/TODO.md

This file was deleted.

4 changes: 2 additions & 2 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,10 @@ export function createAuth<ListTypeInfo extends BaseListTypeInfo>({

async function authMiddleware({
context,
isValidSession: wasAccessAllowed,
wasAccessAllowed,
}: {
context: KeystoneContext;
isValidSession: boolean; // TODO: rename "isValidSession" to "wasAccessAllowed"?
wasAccessAllowed: boolean;
}): Promise<{ kind: 'redirect'; to: string } | void> {
const { req } = context;
const { pathname } = new URL(req!.url!, 'http://_');
Expand Down
15 changes: 8 additions & 7 deletions packages/auth/src/lib/useFromRedirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { useRouter } from '@keystone-6/core/admin-ui/router';

export const useRedirect = () => {
const router = useRouter();
const redirect = useMemo(
() =>
!Array.isArray(router.query.from) && router.query.from?.startsWith('/')
? router.query.from
: '/',
[router]
);
const redirect = useMemo(() => {
const { from } = router.query;
if (typeof from !== 'string') return '/';
if (!from.startsWith('/')) return '/';
if (from === '/no-access') return '/';

return from;
}, [router]);

return redirect;
};
Loading

1 comment on commit 384748d

@vercel
Copy link

@vercel vercel bot commented on 384748d Jan 30, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.