require('dotenv').config(); const { Keystone } = require('@keystonejs/keystone'); const { GraphQLApp } = require('@keystonejs/app-graphql'); const { AdminUIApp } = require('@keystonejs/app-admin-ui'); const { KnexAdapter: Adapter } = require('@keystonejs/adapter-knex'); const { PasswordAuthStrategy } = require('@keystonejs/auth-password'); const MagicLinkStrategy = require('passport-magic-link').Strategy; const passport = require('passport'); const session = require('express-session'); const { signin } = require('./emails'); const gql = require('graphql-tag'); const { User } = require('./schema'); const PROJECT_NAME = 'keystone-magic-link-example'; const knexOptions = require('./knexfile'); const adapterConfig = { knexOptions }; const KnexSessionStore = require('connect-session-knex')(session); const isProduction = process.env.NODE_ENV === 'production'; const keystone = new Keystone({ name: PROJECT_NAME, cookieSecret: process.env.COOKIE_SECRET, cookie: { secure: isProduction, }, adapter: new Adapter(adapterConfig), sessionStore: new KnexSessionStore({ knex: require('knex')(knexOptions), tablename: 'user_sessions', // optional. Defaults to 'sessions' }), }); // user schema keystone.createList('User', User); const authStrategy = keystone.createAuthStrategy({ type: PasswordAuthStrategy, list: 'User', }); function sendToken(user, token) { const url = process.env.SERVER_URL || 'http://localhost:4000'; signin({ to: user.email, subject: 'Sign in', name: user.name, magicLink: `${url}/auth?token=${token}`, }); } async function verifyUser(user) { const context = keystone.createContext({ skipAccessControl: true }); const { data, errors } = await keystone.executeGraphQL({ context, query: gql` query findUser($email: String) { allUsers(where: { email: $email }) { id email name } } `, variables: { email: user.email }, }); const msg = 'Cannot find the user with given email'; if (errors || !data) { console.error(errors); return Promise.reject(msg); } const [usr] = data.allUsers || []; if (!usr) return Promise.reject(msg); console.log(usr); return Promise.resolve(usr); } passport.use( new MagicLinkStrategy( { secret: process.env.TOKEN_SECRET, userFields: ['email'], tokenField: 'token', ttl: process.env.AUTH_TOKEN_EXPIRY, }, sendToken, verifyUser ) ); module.exports = { keystone, apps: [ new GraphQLApp(), new AdminUIApp({ enableDefaultRoute: false, authStrategy, isAccessAllowed: ({ authentication: { item: user } }) => !!user && !!user.isAdmin, }), ], configureExpress: (app) => { app.set('trust proxy', 1); app.use(passport.initialize()); app.use(passport.session()); passport.serializeUser((user, done) => done(null, user)); passport.deserializeUser((user, done) => done(null, user)); app.post( '/auth/magiclink', passport.authenticate('magiclink', { action: 'requestToken' }), (req, res) => res.json({ ok: true }) ); app.get( '/auth/magiclink/callback', passport.authenticate('magiclink', { action: 'acceptToken' }), (req, res) => res.json({ ok: true }) ); // How can this sort of authorisation be used in GraphQLApp? app.get( '/authorized', passport.authenticate('magiclink', { action: 'acceptToken', allowReuse: true, }), (req, res) => { res.json({ user: req.user, query: req.query, }); } ); }, };