Skip to content

Commit

Permalink
Update config.session to accept a sessionStrategy object (#5802)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored May 27, 2021
1 parent bb4f4ac commit 7bda87e
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 130 deletions.
6 changes: 6 additions & 0 deletions .changeset/tasty-stingrays-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@keystone-next/keystone': major
'@keystone-next/types': major
---

Changed `config.session` to access a `SessionStrategy` object, rather than a `() => SessionStrategy` function. You will only need to change your configuration if you're using a customised session strategy.
2 changes: 1 addition & 1 deletion docs/pages/apis/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default withAuth(
isAdmin: checkbox(),
},
}),
session: () => { /* ... */ },
session: { /* ... */ },
}),
})
);
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/apis/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default config({
db: { /* ... */ },
ui: { /* ... */ },
server: { /* ... */ },
session: () => { /* ... */ },
session: { /* ... */ },
graphql: { /* ... */ },
extendGraphqlSchema: { /* ... */ },
images: { /* ... */ },
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/apis/session.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Markdown } from '../../components/Page';
# Session API

The `session` property of the [system configuration](./config) object allows you to configure session management of your Keystone system.
It has a TypeScript type of `() => SessionStrategy<any>`.
In general you will use functions from the `@keystone-next/keystone/session` package, rather than writing this function yourself.
It has a TypeScript type of `SessionStrategy<any>`.
In general you will use `SessionStrategy` objects from the `@keystone-next/keystone/session` package, rather than writing this yourself.

```typescript
import { config } from '@keystone-next/keystone/schema';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function nextGraphQLAPIRoute(keystoneConfig: KeystoneConfig, prismaClient
const apolloServer = createApolloServerMicro({
graphQLSchema,
createContext,
sessionStrategy: initializedKeystoneConfig.session?.(),
sessionStrategy: initializedKeystoneConfig.session,
apolloConfig: initializedKeystoneConfig.graphql?.apolloConfig,
connectionPromise: keystone.connect(),
});
Expand Down
6 changes: 2 additions & 4 deletions packages-next/keystone/src/lib/server/createExpressServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,13 @@ export const createExpressServer = async (
server.use(cors(corsConfig));
}

const sessionStrategy = config.session ? config.session() : undefined;

if (isVerbose) console.log('✨ Preparing GraphQL Server');
addApolloServer({
server,
config,
graphQLSchema,
createContext,
sessionStrategy,
sessionStrategy: config.session,
apolloConfig: config.graphql?.apolloConfig,
});

Expand All @@ -75,7 +73,7 @@ export const createExpressServer = async (
} else {
if (isVerbose) console.log('✨ Preparing Admin UI Next.js app');
server.use(
await createAdminUIServer(config.ui, createContext, dev, projectAdminPath, sessionStrategy)
await createAdminUIServer(config.ui, createContext, dev, projectAdminPath, config.session)
);
}

Expand Down
226 changes: 106 additions & 120 deletions packages-next/keystone/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,43 +81,35 @@ type FieldSelections = {
*/

export function withItemData(
createSession: () => SessionStrategy<Record<string, any>>,
_sessionStrategy: SessionStrategy<Record<string, any>>,
fieldSelections: FieldSelections = {}
): () => SessionStrategy<{ listKey: string; itemId: string; data: any }> {
return (): SessionStrategy<any> => {
const { get, ...sessionStrategy } = createSession();
return {
...sessionStrategy,
get: async ({ req, createContext }) => {
const session = await get({ req, createContext });
const sudoContext = createContext({}).sudo();
if (
!session ||
!session.listKey ||
!session.itemId ||
!sudoContext.lists[session.listKey]
) {
return;
}

// NOTE: This is wrapped in a try-catch block because a "not found" result will currently
// throw; I think this needs to be reviewed, but for now this prevents a system crash when
// the session item is invalid
try {
// If no field selection is specified, just load the id. We still load the item,
// because doing so validates that it exists in the database
const item = await sudoContext.lists[session.listKey].findOne({
where: { id: session.itemId },
query: fieldSelections[session.listKey] || 'id',
});
return { ...session, data: item };
} catch (e) {
// TODO: This swallows all errors, we need a way to differentiate between "not found" and
// actual exceptions that should be thrown
return;
}
},
};
): SessionStrategy<{ listKey: string; itemId: string; data: any }> {
const { get, ...sessionStrategy } = _sessionStrategy;
return {
...sessionStrategy,
get: async ({ req, createContext }) => {
const session = await get({ req, createContext });
const sudoContext = createContext({}).sudo();
if (!session || !session.listKey || !session.itemId || !sudoContext.lists[session.listKey]) {
return;
}
// NOTE: This is wrapped in a try-catch block because a "not found" result will currently
// throw; I think this needs to be reviewed, but for now this prevents a system crash when
// the session item is invalid
try {
// If no field selection is specified, just load the id. We still load the item,
// because doing so validates that it exists in the database
const item = await sudoContext.lists[session.listKey].findOne({
where: { id: session.itemId },
query: fieldSelections[session.listKey] || 'id',
});
return { ...session, listKey: session.listKey, itemId: session.itemId, data: item };
} catch (e) {
// TODO: This swallows all errors, we need a way to differentiate between "not found" and
// actual exceptions that should be thrown
return;
}
},
};
}

Expand All @@ -129,104 +121,98 @@ export function statelessSessions<T>({
ironOptions = Iron.defaults,
domain,
sameSite = 'lax',
}: StatelessSessionsOptions): () => SessionStrategy<T> {
return () => {
if (!secret) {
throw new Error('You must specify a session secret to use sessions');
}
if (secret.length < 32) {
throw new Error('The session secret must be at least 32 characters long');
}
return {
async get({ req }) {
if (!req.headers.cookie) return;
let cookies = cookie.parse(req.headers.cookie);
if (!cookies[TOKEN_NAME]) return;
try {
return await Iron.unseal(cookies[TOKEN_NAME], secret, ironOptions);
} catch (err) {}
},
async end({ res }) {
res.setHeader(
'Set-Cookie',
cookie.serialize(TOKEN_NAME, '', {
maxAge: 0,
expires: new Date(),
httpOnly: true,
secure,
path,
sameSite,
domain,
})
);
},
async start({ res, data }) {
let sealedData = await Iron.seal(data, secret, { ...ironOptions, ttl: maxAge * 1000 });
}: StatelessSessionsOptions): SessionStrategy<T> {
if (!secret) {
throw new Error('You must specify a session secret to use sessions');
}
if (secret.length < 32) {
throw new Error('The session secret must be at least 32 characters long');
}
return {
async get({ req }) {
if (!req.headers.cookie) return;
let cookies = cookie.parse(req.headers.cookie);
if (!cookies[TOKEN_NAME]) return;
try {
return await Iron.unseal(cookies[TOKEN_NAME], secret, ironOptions);
} catch (err) {}
},
async end({ res }) {
res.setHeader(
'Set-Cookie',
cookie.serialize(TOKEN_NAME, '', {
maxAge: 0,
expires: new Date(),
httpOnly: true,
secure,
path,
sameSite,
domain,
})
);
},
async start({ res, data }) {
let sealedData = await Iron.seal(data, secret, { ...ironOptions, ttl: maxAge * 1000 });

res.setHeader(
'Set-Cookie',
cookie.serialize(TOKEN_NAME, sealedData, {
maxAge,
expires: new Date(Date.now() + maxAge * 1000),
httpOnly: true,
secure,
path,
sameSite,
domain,
})
);
res.setHeader(
'Set-Cookie',
cookie.serialize(TOKEN_NAME, sealedData, {
maxAge,
expires: new Date(Date.now() + maxAge * 1000),
httpOnly: true,
secure,
path,
sameSite,
domain,
})
);

return sealedData;
},
};
return sealedData;
},
};
}

export function storedSessions({
store: storeOption,
maxAge = MAX_AGE,
...statelessSessionsOptions
}: {
store: SessionStoreFunction;
} & StatelessSessionsOptions): () => SessionStrategy<JSONValue> {
return () => {
let { get, start, end } = statelessSessions({ ...statelessSessionsOptions, maxAge })();
let store = typeof storeOption === 'function' ? storeOption({ maxAge }) : storeOption;
let isConnected = false;
return {
async get({ req, createContext }) {
const data = (await get({ req, createContext })) as { sessionId: string } | undefined;
const sessionId = data?.sessionId;
if (typeof sessionId === 'string') {
if (!isConnected) {
await store.connect?.();
isConnected = true;
}
return store.get(sessionId);
}
},
async start({ res, data, createContext }) {
let sessionId = generateSessionId();
}: { store: SessionStoreFunction } & StatelessSessionsOptions): SessionStrategy<JSONValue> {
let { get, start, end } = statelessSessions({ ...statelessSessionsOptions, maxAge });
let store = typeof storeOption === 'function' ? storeOption({ maxAge }) : storeOption;
let isConnected = false;
return {
async get({ req, createContext }) {
const data = (await get({ req, createContext })) as { sessionId: string } | undefined;
const sessionId = data?.sessionId;
if (typeof sessionId === 'string') {
if (!isConnected) {
await store.connect?.();
isConnected = true;
}
await store.set(sessionId, data);
return start?.({ res, data: { sessionId }, createContext }) || '';
},
async end({ req, res, createContext }) {
const data = (await get({ req, createContext })) as { sessionId: string } | undefined;
const sessionId = data?.sessionId;
if (typeof sessionId === 'string') {
if (!isConnected) {
await store.connect?.();
isConnected = true;
}
await store.delete(sessionId);
return store.get(sessionId);
}
},
async start({ res, data, createContext }) {
let sessionId = generateSessionId();
if (!isConnected) {
await store.connect?.();
isConnected = true;
}
await store.set(sessionId, data);
return start?.({ res, data: { sessionId }, createContext }) || '';
},
async end({ req, res, createContext }) {
const data = (await get({ req, createContext })) as { sessionId: string } | undefined;
const sessionId = data?.sessionId;
if (typeof sessionId === 'string') {
if (!isConnected) {
await store.connect?.();
isConnected = true;
}
await end?.({ req, res, createContext });
},
};
await store.delete(sessionId);
}
await end?.({ req, res, createContext });
},
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages-next/types/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type KeystoneConfig = {
db: DatabaseConfig;
ui?: AdminUIConfig;
server?: ServerConfig;
session?: () => SessionStrategy<any>;
session?: SessionStrategy<any>;
graphql?: GraphQLConfig;
extendGraphqlSchema?: ExtendGraphqlSchema;
files?: FilesConfig;
Expand Down

1 comment on commit 7bda87e

@vercel
Copy link

@vercel vercel bot commented on 7bda87e May 27, 2021

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.