keystone-6-oauth
Version:
Keystone6 Plugin that enables social logins such as Google, Twitter, Github, Facebook and others.
464 lines (437 loc) • 16.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties');
var _includesInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/includes');
var _mapInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/map');
var _JSON$stringify = require('@babel/runtime-corejs3/core-js-stable/json/stringify');
var _startsWithInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/starts-with');
var cookie = require('cookie');
var react = require('next-auth/react');
var jwt = require('next-auth/jwt');
var ejs = require('ejs');
var _filterInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/filter');
var core = require('@keystone-6/core');
var url = require('url');
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
var _includesInstanceProperty__default = /*#__PURE__*/_interopDefault(_includesInstanceProperty);
var _mapInstanceProperty__default = /*#__PURE__*/_interopDefault(_mapInstanceProperty);
var _JSON$stringify__default = /*#__PURE__*/_interopDefault(_JSON$stringify);
var _startsWithInstanceProperty__default = /*#__PURE__*/_interopDefault(_startsWithInstanceProperty);
var cookie__namespace = /*#__PURE__*/_interopNamespace(cookie);
var ejs__default = /*#__PURE__*/_interopDefault(ejs);
var _filterInstanceProperty__default = /*#__PURE__*/_interopDefault(_filterInstanceProperty);
var url__default = /*#__PURE__*/_interopDefault(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__default["default"].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__default["default"].render(template, {
autoCreate,
identityField,
listKey,
sessionData,
sessionSecret
});
return authOut;
};
function getBaseAuthSchema(_ref) {
let {
listKey,
base
} = _ref;
const extension = {
query: {
authenticatedItem: core.graphql.field({
type: core.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 core.graphql.extend(base => {
var _context;
const baseSchema = getBaseAuthSchema({
base,
listKey
});
return _filterInstanceProperty__default["default"](_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__default["default"].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__default["default"](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__default["default"](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__default["default"](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__namespace.serialize(TOKEN_NAME, '', {
// TODO: Update parse to URL
domain: url__default["default"].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__default["default"].parse(req === null || req === void 0 ? void 0 : req.url).pathname;
// TODO
let nextSession = null;
if (!req) return;
if (_includesInstanceProperty__default["default"](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 jwt.getToken({
req: req,
secret: sessionSecret
});
} else {
nextSession = await react.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__default["default"].parse((_context$req = context.req) === null || _context$req === void 0 ? void 0 : _context$req.url).pathname;
if (_startsWithInstanceProperty__default["default"](pathname).call(pathname, `${customPath}/_next`) || _startsWithInstanceProperty__default["default"](pathname).call(pathname, `${customPath}/__next`) || _startsWithInstanceProperty__default["default"](pathname).call(pathname, `${customPath}/api/auth/`) || pages !== null && pages !== void 0 && pages.signIn && _includesInstanceProperty__default["default"](pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.signIn) || pages !== null && pages !== void 0 && pages.error && _includesInstanceProperty__default["default"](pathname).call(pathname, pages === null || pages === void 0 ? void 0 : pages.error) || pages !== null && pages !== void 0 && pages.signOut && _includesInstanceProperty__default["default"](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
};
}
exports.createAuth = createAuth;