Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ecommerce example to match Wes' course #4781

Merged
merged 3 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
1,164 changes: 1,164 additions & 0 deletions examples-next/ecommerce/.keystone/schema-types.ts

Large diffs are not rendered by default.

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