Skip to content

Commit

Permalink
Update ecommerce example to match Wes' course
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie committed Feb 7, 2021
1 parent dfc7249 commit ff90dae
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 552 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-countries-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/example-ecommerce': major
---

Updated all code to match the released version of Wes' course.
5 changes: 5 additions & 0 deletions .changeset/tame-radios-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/example-ecommerce': patch
---

Added missing `returnFields` in `addToCart` resolver.
102 changes: 46 additions & 56 deletions examples-next/ecommerce/access.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,82 @@
import { permissionsList } from './schemas/fields';
import { ListAccessArgs } from './types';
// At it's simplest, the access control returns a yes or no value depending on the users session

/*
The basic level of access to the system is being signed in as a valid user. This gives you access
to the Admin UI, access to your own User and Todo items, and read access to roles.
*/
export const isSignedIn = ({ session }: ListAccessArgs) => {
export function isSignedIn({ session }: ListAccessArgs) {
return !!session;
};
}

/*
Permissions are shorthand functions for checking that the current user's role has the specified
permission boolean set to true
*/
const generatedPermissions = Object.fromEntries(
permissionsList.map(permission => [
permission,
function ({ session }: ListAccessArgs) {
// Do they have that Permission? Yes or no
return !!session?.data.role?.[permission];
},
])
);

// Permissions check if someone meets a criteria - yes or no.
export const permissions = {
// We create a permission for each can* field on the Role type
...generatedPermissions,
// we can also add additional permissions as we need them
isAwesome({ session }: ListAccessArgs) {
if (session?.data.name?.includes('wes') || session?.data.name?.includes('jed')) {
// they are awesome, let them have access
return true;
}
return false; // not awesome, no access
isAwesome({ session }: ListAccessArgs): boolean {
return !!session?.data.name.includes('wes');
},
};

/*
Rules are logical functions that can be used for list access, and may return a boolean (meaning
all or no items are available) or a set of filters that limit the available items
*/
// Rule based function
// Rules can return a boolean - yes or no - or a filter which limits which products they can CRUD.
export const rules = {
canOrder: ({ session }: ListAccessArgs) => {
if (!session) return false; // not signed in
if (permissions.canManageCart(session)) return true; // if they have the permission
// otherwise we only show them cart items that they own
return {
user: { id: session.itemId },
};
canManageProducts({ session }: ListAccessArgs) {
if (!isSignedIn({ session })) {
return false;
}
// 1. Do they have the permission of canManageProducts
if (permissions.canManageProducts({ session })) {
return true;
}
// 2. If not, do they own this item?
return { user: { id: session?.itemId } };
},
canReadUsers: ({ session }: ListAccessArgs) => {
if (!session) {
// No session? No people.
canOrder({ session }: ListAccessArgs) {
if (!isSignedIn({ session })) {
return false;
} else if (session.data.role?.canSeeOtherUsers) {
// Can see everyone
}
// 1. Do they have the permission of canManageProducts
if (permissions.canManageCart({ session })) {
return true;
} else {
// Can only see yourself
return { id: session.itemId };
}
// 2. If not, do they own this item?
return { user: { id: session?.itemId } };
},
canUpdateUsers: ({ session }: ListAccessArgs) => {
if (!session) {
// No session? No people.
canManageOrderItems({ session }: ListAccessArgs) {
if (!isSignedIn({ session })) {
return false;
} else if (session.data.role?.canManageUsers) {
// Can update everyone
}
// 1. Do they have the permission of canManageProducts
if (permissions.canManageCart({ session })) {
return true;
} else {
// Can update yourself
return { id: session.itemId };
}
// 2. If not, do they own this item?
return { order: { user: { id: session?.itemId } } };
},
canUpdateProducts({ session }: ListAccessArgs) {
// Do they have access?
canReadProducts({ session }: ListAccessArgs) {
if (!isSignedIn({ session })) {
return false;
}
if (permissions.canManageProducts({ session })) {
// They have the permission
return true;
return true; // They can read everything!
}
// Otherwise, only allow them to manage their own products
return { user: { id: session?.itemId } };
// They should only see available products (based on the status field)
return { status: 'AVAILABLE' };
},
canReadProducts: ({ session }: ListAccessArgs) => {
if (session?.data.role?.canManageProducts) {
canManageUsers({ session }: ListAccessArgs) {
if (!isSignedIn({ session })) {
return false;
}
if (permissions.canManageUsers({ session })) {
return true;
} else {
return { status: 'AVAILABLE' };
}
// Otherwise they may only update themselves!
return { id: session?.itemId };
},
};
56 changes: 26 additions & 30 deletions examples-next/ecommerce/keystone.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createAuth } from '@keystone-next/auth';
import { config, createSchema } from '@keystone-next/keystone/schema';
import { withItemData, statelessSessions } from '@keystone-next/keystone/session';
import { permissionsList } from './schemas/fields';
import { Role } from './schemas/Role';
import { OrderItem } from './schemas/OrderItem';
import { Order } from './schemas/Order';
Expand All @@ -6,80 +10,72 @@ import { ProductImage } from './schemas/ProductImage';
import { Product } from './schemas/Product';
import { User } from './schemas/User';
import 'dotenv/config';

import { config, createSchema } from '@keystone-next/keystone/schema';
import { statelessSessions, withItemData } from '@keystone-next/keystone/session';
import { extendGraphqlSchema } from './mutations';
import { createAuth } from '@keystone-next/auth';
import { insertSeedData } from './seed-data';
import { permissionsList } from './schemas/fields';
import sendPasswordResetEmail from './lib/sendPasswordResetEmail';
import { sendPasswordResetEmail } from './lib/mail';
import { extendGraphqlSchema } from './mutations';

const databaseURL = process.env.DATABASE_URL || 'mongodb://localhost/keystone-sick-fits-tutorial';

const databaseUrl = process.env.DATABASE_URL || 'mongodb://localhost/keystone-examples-ecommerce';
const protectIdentities = process.env.NODE_ENV === 'production';
const sessionConfig = {
maxAge: 60 * 60 * 24 * 30, // 30 days
secret: process.env.COOKIE_SECRET || '',
maxAge: 60 * 60 * 24 * 360, // How long they stay signed in?
secret: process.env.COOKIE_SECRET!,
};

const { withAuth } = createAuth({
listKey: 'User',
identityField: 'email',
secretField: 'password',
protectIdentities,
initFirstItem: {
fields: ['name', 'email', 'password'],
itemData: {
role: {
create: {
name: 'Admin Role',
...Object.fromEntries(permissionsList.map(i => [i, true])),
},
},
},
// TODO: Add in inital roles here
},
passwordResetLink: {
async sendToken(args) {
// send the email
await sendPasswordResetEmail(args.token, args.identity);
console.log(`Password reset info:`, args);
},
},
});

export default withAuth(
config({
// @ts-ignore
server: {
cors: {
origin: ['http://localhost:2223'],
origin: [process.env.FRONTEND_URL!],
credentials: true,
},
},
db: {
adapter: 'mongoose',
url: databaseUrl,
onConnect: async ({ keystone }) => {
url: databaseURL,
async onConnect(keystone) {
console.log('Connected to the database!');
if (process.argv.includes('--seed-data')) {
insertSeedData(keystone);
await insertSeedData(keystone);
}
},
},
lists: createSchema({
// Schema items go in here
User,
Product,
ProductImage,
CartItem,
Order,
OrderItem,
Order,
Role,
}),
extendGraphqlSchema,
ui: {
// Anyone who has been assigned a role can access the Admin UI
isAccessAllowed: ({ session }) => !!session?.data.role,
// Show the UI only for poeple who pass this test
isAccessAllowed: ({ session }) =>
// console.log(session);
!!session?.data,
},
session: withItemData(statelessSessions(sessionConfig), {
// Cache the permission fields from the role in the session so we don't have to look them up again in access checks
User: `id name role { ${permissionsList.join(' ')} }`,
// GraphQL Query
User: `id name email role { ${permissionsList.join(' ')} }`,
}),
})
);
39 changes: 34 additions & 5 deletions examples-next/ecommerce/lib/mail.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import nodemailer from 'nodemailer';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { createTransport, getTestMessageUrl } from 'nodemailer';

const transport = nodemailer.createTransport({
const transport = createTransport({
// @ts-ignore
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS,
},
} as SMTPTransport.Options);
});

function makeANiceEmail(text: string) {
return `
Expand All @@ -27,4 +27,33 @@ function makeANiceEmail(text: string) {
`;
}

export { makeANiceEmail, transport };
export interface MailResponse {
accepted?: string[] | null;
rejected?: null[] | null;
envelopeTime: number;
messageTime: number;
messageSize: number;
response: string;
envelope: Envelope;
messageId: string;
}
export interface Envelope {
from: string;
to?: string[] | null;
}

export async function sendPasswordResetEmail(resetToken: string, to: string): Promise<void> {
// email the user a token
const info = (await transport.sendMail({
to,
from: '[email protected]',
subject: 'Your password reset token!',
html: makeANiceEmail(`Your Password Reset Token is here!
<a href="${process.env.FRONTEND_URL}/reset?token=${resetToken}">Click Here to reset</a>
`),
})) as MailResponse;
if (process.env.MAIL_USER?.includes('ethereal.email')) {
// @ts-ignore
console.log(`� Message Sent! Preview it at ${getTestMessageUrl(info)}`);
}
}
56 changes: 0 additions & 56 deletions examples-next/ecommerce/lib/sendPasswordResetEmail.ts

This file was deleted.

Loading

0 comments on commit ff90dae

Please sign in to comment.