keystone-6-oauth
Version:
Keystone6 Plugin that enables social logins such as Google, Twitter, Github, Facebook and others.
431 lines (409 loc) • 14.4 kB
JavaScript
import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
import _includesInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/includes';
import _mapInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/map';
import _JSON$stringify from '@babel/runtime-corejs3/core-js-stable/json/stringify';
import _startsWithInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/starts-with';
import * as cookie from 'cookie';
import { getSession } from 'next-auth/react';
import { getToken } from 'next-auth/jwt';
import ejs from 'ejs';
import _filterInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/filter';
import { graphql } from '@keystone-6/core';
import url from 'url';
const template$1 = `
const keystoneConfig = require('@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/next-config').config;
module.exports = {
...keystoneConfig,
basePath: '<%= keystonePath || '' %>'
};
`;
const nextConfigTemplate = _ref => {
let {
keystonePath
} = _ref;
const nextConfigOut = ejs.render(template$1, {
keystonePath
});
return nextConfigOut;
};
const template = `
import { getContext } from '@keystone-6/core/context';
import getNextAuthPage from 'keystone-6-oauth/pages/NextAuthPage';
import keystoneConfig from '../../../../../keystone';
import * as PrismaModule from '.prisma/client';
const keystoneContext = global.keystoneContext || getContext(keystoneConfig, PrismaModule);
if (process.env.NODE_ENV !== 'production') globalThis.keystoneContext = keystoneContext
export default getNextAuthPage({
autoCreate: <%= autoCreate %>,
context: keystoneContext,
identityField: '<%= identityField %>',
listKey: '<%= listKey %>',
onSignIn: keystoneConfig.onSignIn,
onSignUp: keystoneConfig.onSignUp,
pages: keystoneConfig.pages,
providers: keystoneConfig.providers,
sessionData: '<%= sessionData %>',
sessionSecret: '<%= sessionSecret %>',
});
`;
const authTemplate = _ref => {
let {
autoCreate,
identityField,
listKey,
sessionData,
sessionSecret
} = _ref;
const authOut = ejs.render(template, {
autoCreate,
identityField,
listKey,
sessionData,
sessionSecret
});
return authOut;
};
function getBaseAuthSchema(_ref) {
let {
listKey,
base
} = _ref;
const extension = {
query: {
authenticatedItem: graphql.field({
type: graphql.union({
name: 'AuthenticatedItem',
types: [base.object(listKey)],
resolveType: (root, context) => {
var _context$session;
return (_context$session = context.session) === null || _context$session === void 0 ? void 0 : _context$session.listKey;
}
}),
resolve(root, args, _ref2) {
let {
session,
db
} = _ref2;
if (typeof (session === null || session === void 0 ? void 0 : session.itemId) === 'string' && typeof session.listKey === 'string') {
return db[session.listKey].findOne({
where: {
id: session.itemId
}
});
}
return null;
}
})
}
};
return {
extension
};
}
const getSchemaExtension = _ref => {
let {
listKey
} = _ref;
return graphql.extend(base => {
var _context;
const baseSchema = getBaseAuthSchema({
base,
listKey
});
return _filterInstanceProperty(_context = [baseSchema.extension]).call(_context, x => x !== undefined);
});
};
const _excluded = ["get", "end"];
/**
* createAuth function
*
* Generates config for Keystone to implement standard auth features.
*/
function createAuth(_ref) {
let {
autoCreate,
context,
cookies,
identityField,
listKey,
keystonePath,
onSignIn,
onSignUp,
pages,
providers,
sessionData,
sessionSecret
} = _ref;
// The protectIdentities flag is currently under review to see whether it should be
// part of the createAuth API (in which case its use cases need to be documented and tested)
// or whether always being true is what we want, in which case we can refactor our code
// to match this. -TL
const customPath = !keystonePath || keystonePath === '/' ? '' : keystonePath;
/**
* pageMiddleware
*
* Should be added to the ui.pageMiddleware stack.
*
* Redirects:
* - from the signin or init pages to the index when a valid session is present
* - to the init page when initFirstItem is configured, and there are no user in the database
* - to the signin page when no valid session is present
*/
// TODO: [TYPES] Check pageMiddleware
const authMiddleware = async _ref2 => {
let {
context,
isValidSession
} = _ref2;
const {
req,
session
} = context;
const pathname = url.parse(req === null || req === void 0 ? void 0 : req.url).pathname;
if (isValidSession) {
if (customPath !== '' && pathname === '/') {
return {
kind: 'redirect',
to: `${customPath}`
};
}
return null;
}
if (!session && !_includesInstanceProperty(pathname).call(pathname, `${customPath}/api/auth/`)) {
return {
kind: 'redirect',
to: (pages === null || pages === void 0 ? void 0 : pages.signIn) || `${customPath}/api/auth/signin`
};
}
};
/**
* authGetAdditionalFiles
*
* This function adds files to be generated into the Admin UI build. Must be added to the
* ui.getAdditionalFiles config.
*
* The sign-in page is always included, and the init page is included when initFirstItem is set
*/
const authGetAdditionalFiles = () => {
const filesToWrite = [{
mode: 'write',
outputPath: 'pages/api/auth/[...nextauth].js',
src: authTemplate({
autoCreate,
identityField,
listKey,
sessionData,
sessionSecret
})
}, {
mode: 'write',
outputPath: 'next.config.js',
src: nextConfigTemplate({
keystonePath: customPath
})
}];
return filesToWrite;
};
/**
* publicAuthPages
*
* Must be added to the ui.publicPages config
*/
const authPublicPages = [`${customPath}/api/__keystone_api_build`, `${customPath}/api/auth/csrf`, `${customPath}/api/auth/signin`, `${customPath}/api/auth/callback`, `${customPath}/api/auth/session`, `${customPath}/api/auth/providers`, `${customPath}/api/auth/signout`, `${customPath}/api/auth/error`];
// TODO: [TYPES] Add Provider
// @ts-ignore
function addPages(provider) {
const name = provider.id;
authPublicPages.push(`${customPath}/api/auth/signin/${name}`);
authPublicPages.push(`${customPath}/api/auth/callback/${name}`);
}
_mapInstanceProperty(providers).call(providers, addPages);
/**
* extendGraphqlSchema
*
* Must be added to the extendGraphqlSchema config. Can be composed.
*/
const extendGraphqlSchema = getSchemaExtension({
identityField,
listKey
});
/**
* validateConfig
*
* Validates the provided auth config; optional step when integrating auth
*/
const validateConfig = keystoneConfig => {
const listConfig = keystoneConfig.lists[listKey];
if (listConfig === undefined) {
const msg = `A createAuth() invocation specifies the list "${listKey}" but no list with that key has been defined.`;
throw new Error(msg);
}
// TODO: Check for String-like typing for identityField? How?
// TODO: Validate that the identifyField is unique.
// TODO: If this field isn't required, what happens if I try to log in as `null`?
const identityFieldConfig = listConfig.fields[identityField];
if (identityFieldConfig === undefined) {
const identityFieldName = _JSON$stringify(identityField);
const msg = `A createAuth() invocation for the "${listKey}" list specifies ${identityFieldName} as its identityField but no field with that key exists on the list.`;
throw new Error(msg);
}
};
/**
* withItemData
*
* Automatically injects a session.data value with the authenticated item
*/
/* TODO:
- [ ] We could support additional where input to validate item sessions (e.g an isEnabled boolean)
*/
const withItemData = _sessionStrategy => {
const {
get,
end
} = _sessionStrategy,
sessionStrategy = _objectWithoutProperties(_sessionStrategy, _excluded);
return _objectSpread(_objectSpread({}, sessionStrategy), {}, {
end: async _ref3 => {
let {
context
} = _ref3;
await end({
context
});
const TOKEN_NAME = process.env.NODE_ENV === 'development' ? 'next-auth.session-token' : '__Secure-next-auth.session-token';
const {
req,
res
} = context;
if (!req || !res) return;
res.setHeader('Set-Cookie', cookie.serialize(TOKEN_NAME, '', {
// TODO: Update parse to URL
domain: url.parse(req.url).hostname,
expires: new Date(),
httpOnly: true,
maxAge: 0,
path: '/',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production'
}));
},
// TODO: [TYPES] Add get typing
// @ts-ignore
get: async _ref4 => {
var _req$headers, _req$headers$authoriz;
let {
context
} = _ref4;
const {
req
} = context;
const pathname = url.parse(req === null || req === void 0 ? void 0 : req.url).pathname;
// TODO
let nextSession = null;
if (!req) return;
if (_includesInstanceProperty(pathname).call(pathname, '/api/auth')) {
return null;
}
const sudoContext = context.sudo();
if (((_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : (_req$headers$authoriz = _req$headers.authorization) === null || _req$headers$authoriz === void 0 ? void 0 : _req$headers$authoriz.split(' ')[0]) === 'Bearer') {
nextSession = await getToken({
req: req,
secret: sessionSecret
});
} else {
nextSession = await getSession({
req
}); // TODO: [TYPES] Review nextSession
}
if (!nextSession || !nextSession.listKey || nextSession.listKey !== listKey || !nextSession.itemId || !sudoContext.query[listKey] || !nextSession.itemId) {
return null;
}
const reqWithUser = req;
reqWithUser.user = {
data: nextSession.data,
itemId: nextSession.itemId,
listKey: nextSession.listKey
};
const userSession = await get({
context
});
return _objectSpread(_objectSpread(_objectSpread({}, userSession), nextSession), {}, {
data: nextSession.data,
itemId: nextSession.itemId,
listKey: nextSession.listKey
});
}
});
};
function defaultIsAccessAllowed(_ref5) {
let {
session
} = _ref5;
return session !== undefined;
}
/**
* withAuth
*
* Automatically extends config with the correct auth functionality. This is the easiest way to
* configure auth for keystone; you should probably use it unless you want to extend or replace
* the way auth is set up with custom functionality.
*
* It validates the auth config against the provided keystone config, and preserves existing
* config by composing existing extendGraphqlSchema functions and ui config.
*/
const withAuth = keystoneConfig => {
var _ui;
validateConfig(keystoneConfig);
let {
ui
} = keystoneConfig;
if (!((_ui = ui) !== null && _ui !== void 0 && _ui.isDisabled)) {
const {
getAdditionalFiles = [],
isAccessAllowed = defaultIsAccessAllowed,
pageMiddleware,
publicPages = []
} = ui || {};
ui = _objectSpread(_objectSpread({}, ui), {}, {
publicPages: [...publicPages, ...authPublicPages],
isAccessAllowed: async context => {
var _context$req;
const pathname = url.parse((_context$req = context.req) === null || _context$req === void 0 ? void 0 : _context$req.url).pathname;
if (_startsWithInstanceProperty(pathname).call(pathname, `${customPath}/_next`) || _startsWithInstanceProperty(pathname).call(pathname, `${customPath}/__next`) || _startsWithInstanceProperty(pathname).call(pathname, `${customPath}/api/auth/`) || pages !== null && pages !== void 0 && pages.signIn && _includesInstanceProperty(pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.signIn) || pages !== null && pages !== void 0 && pages.error && _includesInstanceProperty(pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.error) || pages !== null && pages !== void 0 && pages.signOut && _includesInstanceProperty(pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.signOut)) {
return true;
}
return await isAccessAllowed(context);
},
getAdditionalFiles: [...getAdditionalFiles, authGetAdditionalFiles],
pageMiddleware: async args => {
if (!authMiddleware) throw new Error('Missing authMiddleware');
const shouldRedirect = await authMiddleware(args);
if (shouldRedirect) return shouldRedirect;
return pageMiddleware === null || pageMiddleware === void 0 ? void 0 : pageMiddleware(args);
}
});
}
if (!keystoneConfig.session) throw new TypeError('Missing .session configuration');
const session = withItemData(keystoneConfig.session);
const existingExtendGraphQLSchema = keystoneConfig.extendGraphqlSchema;
return _objectSpread(_objectSpread({}, keystoneConfig), {}, {
context,
cookies,
extendGraphqlSchema: existingExtendGraphQLSchema
// TODO: [TYPES] Add schema
? schema => existingExtendGraphQLSchema(extendGraphqlSchema(schema)) : extendGraphqlSchema,
lists: _objectSpread({}, keystoneConfig.lists),
onSignIn,
onSignUp,
pages,
providers,
session,
ui
});
};
return {
withAuth
};
}
export { createAuth };