Skip to content

Commit

Permalink
docs(examples): add passportjs usecase
Browse files Browse the repository at this point in the history
  • Loading branch information
iamandrewluca committed May 1, 2024
1 parent bd33f07 commit e8e2ca7
Show file tree
Hide file tree
Showing 9 changed files with 2,659 additions and 36 deletions.
1 change: 1 addition & 0 deletions examples/usecase-passport/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
52 changes: 52 additions & 0 deletions examples/usecase-passport/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# TODO: Update README

Welcome to Keystone!

Run

```
yarn dev
```

To view the config for your new app, look at [./keystone.ts](./keystone.ts)

This project starter is designed to give you a sense of the power Keystone can offer you, and show off some of its main features. It's also a pretty simple setup if you want to build out from it.

We recommend you use this alongside our [getting started walkthrough](https://keystonejs.com/docs/walkthroughs/getting-started-with-create-keystone-app) which will walk you through what you get as part of this starter.

If you want an overview of all the features Keystone offers, check out our [features](https://keystonejs.com/why-keystone#features) page.

## Some Quick Notes On Getting Started

### Changing the database

We've set you up with an [SQLite database](https://keystonejs.com/docs/apis/config#sqlite) for ease-of-use. If you're wanting to use PostgreSQL, you can!

Just change the `db` property on line 16 of the Keystone file [./keystone.ts](./keystone.ts) to

```typescript
db: {
provider: 'postgresql',
url: process.env.DATABASE_URL || 'DATABASE_URL_TO_REPLACE',
}
```

And provide your database url from PostgreSQL.

For more on database configuration, check out or [DB API Docs](https://keystonejs.com/docs/apis/config#db)

### Auth

We've put auth into its own file to make this humble starter easier to navigate. To explore it without auth turned on, comment out the `isAccessAllowed` on line 21 of the Keystone file [./keystone.ts](./keystone.ts).

For more on auth, check out our [Authentication API Docs](https://keystonejs.com/docs/apis/auth#authentication-api)

### Adding a frontend

As a Headless CMS, Keystone can be used with any frontend that uses GraphQL. It provides a GraphQL endpoint you can write queries against at `/api/graphql` (by default [http://localhost:3000/api/graphql](http://localhost:3000/api/graphql)). At Thinkmill, we tend to use [Next.js](https://nextjs.org/) and [Apollo GraphQL](https://www.apollographql.com/docs/react/get-started/) as our frontend and way to write queries, but if you have your own favourite, feel free to use it.

A walkthrough on how to do this is forthcoming, but in the meantime our [todo example](https://github.com/keystonejs/keystone-react-todo-demo) shows a Keystone set up with a frontend. For a more full example, you can also look at an example app we built for [Prisma Day 2021](https://github.com/keystonejs/prisma-day-2021-workshop)

### Embedding Keystone in a Next.js frontend

While Keystone works as a standalone app, you can embed your Keystone app into a [Next.js](https://nextjs.org/) app. This is quite a different setup to the starter, and we recommend checking out our walkthrough for that [here](https://keystonejs.com/docs/walkthroughs/embedded-mode-with-sqlite-nextjs#how-to-embed-keystone-sq-lite-in-a-next-js-app).
134 changes: 134 additions & 0 deletions examples/usecase-passport/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { randomBytes } from "crypto";
import { Router } from "express";
import { Passport, Profile } from "passport";
import { Strategy, StrategyOptions } from "passport-github2";
import { hashSync } from "bcryptjs";
import { createAuth } from "@keystone-6/auth";
import { statelessSessions } from "@keystone-6/core/session";
import type { KeystoneContext } from "@keystone-6/core/types";
import type { VerifyCallback, VerifyFunction } from "passport-oauth2";
import type { Lists, TypeInfo } from ".keystone/types";
import type { User as PrismaUser } from ".myprisma/client";

type BaseSession<TData extends Record<string, unknown>> = {
listKey: string;
itemId: string;
// Usually is added by @keystone-6/auth if enabled
data?: TData;
};

type SessionData = {
name: string;
createdAt: string;
};

export type Session = BaseSession<SessionData>;

export const { withAuth } = createAuth<Lists.User.TypeInfo<Session>>({
listKey: "User",
identityField: "email",
// Strictly typed sessionData (see Join at the end)
sessionData: "name createdAt" satisfies Join<keyof SessionData>,
secretField: "password",
});

export const session = statelessSessions<Session>({
maxAge: 60 * 60 * 24 * 30,
secret: process.env.SESSION_SECRET!,
});

// From here down is related to Passport Authentication

declare global {
namespace Express {
// Augment the global user added by Passport to be the same as the Prisma User
interface User extends Omit<PrismaUser, "password"> {}
}
}

const options: StrategyOptions = {
// https://github.com/settings/applications/new
clientID: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
scope: ["user:email"],
callbackURL: "http://localhost:3000/auth/github/callback",
};

export function createAuthenticationMiddleware(
commonContext: KeystoneContext<TypeInfo<Session>>
): Router {
const router = Router();
const instance = new Passport();
const strategy = new Strategy(options, createVerify(commonContext));

instance.use(strategy);

const middleware = instance.authenticate("github", {
// No need to use express-session internally
session: false,
});

router.get("/auth/github", middleware);
router.get("/auth/github/callback", middleware, async (req, res) => {
const context = await commonContext.withRequest(req, res);

// Start the session in the same way authenticateItemWithPassword does
// see: packages/auth/src/gql/getBaseAuthSchema.ts
await context.sessionStrategy?.start({
context,
data: {
listKey: "User",
itemId: req.user?.id!,
},
});

res.redirect("/auth/session");
});

// This URL will show current session object
router.get("/auth/session", async (req, res) => {
const context = await commonContext.withRequest(req, res);
const session = await context.sessionStrategy?.get({ context });
res.setHeader("Content-Type", "application/json");
res.send(JSON.stringify(session));
res.end();
});

return router;
}

function createVerify(context: KeystoneContext<TypeInfo>): VerifyFunction {
return async (_a: string, _r: string, p: Profile, done: VerifyCallback) => {
const name = p.displayName;
const email = p.emails?.map((e) => e.value).at(0);

if (!email || !name) {
return done(new Error("No email or name"));
}

// Find or create user with this email
const user = await context.prisma.user.upsert({
where: { email },
create: {
email,
name,
// Generate random password
password: hashSync(randomBytes(32).toString("hex"), 10),
},
update: { name },
select: {
id: true,
email: true,
name: true,
createdAt: true,
},
});

return done(null, user);
};
}

// TODO: Remove this?
type Join<T extends string, U extends string = T> =
| U
| (T extends any ? `${T} ${Join<Exclude<U, T>>}` : never);
25 changes: 25 additions & 0 deletions examples/usecase-passport/keystone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import "dotenv/config";
import { config } from "@keystone-6/core";
import { lists } from "./schema";
import { withAuth, session, createAuthenticationMiddleware } from "./auth";
import { fixPrismaPath } from "../example-utils";

export default withAuth(
config({
db: {
provider: "sqlite",
url: "file:./keystone.db",

// WARNING: this is only needed for our monorepo examples, dont do this
...fixPrismaPath,
},
lists,
session,

server: {
extendExpressApp(app, context) {
app.use(createAuthenticationMiddleware(context));
},
},
})
);
31 changes: 31 additions & 0 deletions examples/usecase-passport/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "keystone-app",
"version": "1.0.2",
"private": true,
"scripts": {
"dev": "keystone dev",
"start": "keystone start",
"build": "keystone build",
"postinstall": "keystone build --no-ui --frozen"
},
"dependencies": {
"@keystone-6/auth": "^7.0.0",
"@keystone-6/core": "^5.0.0",
"@keystone-6/fields-document": "^7.0.0",
"@prisma/client": "^5.13.0",
"@types/bcryptjs": "^2.4.2",
"@types/express": "^4.17.14",
"@types/passport": "^1.0.16",
"@types/passport-github2": "^1.2.9",
"@types/passport-oauth2": "^1.4.15",
"bcryptjs": "^2.4.3",
"dotenv": "^16.0.0",
"express": "^4.17.1",
"passport": "^0.7.0",
"passport-github2": "^0.1.12",
"typescript": "^4.9.5"
},
"devDependencies": {
"prisma": "^5.13.0"
}
}

0 comments on commit e8e2ca7

Please sign in to comment.