-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(examples): add passportjs usecase
- Loading branch information
1 parent
bd33f07
commit 3a74499
Showing
9 changed files
with
2,659 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Keystone Project Starter | ||
|
||
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { randomBytes } from "crypto"; | ||
import { Router } from "express"; | ||
import { Passport } 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"; | ||
|
||
export type Session = { | ||
listKey: "User"; | ||
itemId: string; | ||
data?: { | ||
name: string; | ||
createdAt: string; | ||
}; | ||
}; | ||
|
||
export const { withAuth } = createAuth<Lists.User.TypeInfo<Session>>({ | ||
listKey: "User", | ||
identityField: "email", | ||
sessionData: "name createdAt" satisfies 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", { | ||
session: false, | ||
}); | ||
|
||
router.get("/auth/session", async (req, res) => { | ||
const context = await commonContext.withRequest(req, res); | ||
const session = await context.sessionStrategy?.get({ context }); | ||
res.send(JSON.stringify(session)); | ||
res.end(); | ||
}); | ||
|
||
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"); | ||
}); | ||
|
||
return router; | ||
} | ||
|
||
// This is how GitHub user profiles look like | ||
type Profile = { | ||
displayName?: string; | ||
emails?: { value?: string }[]; | ||
}; | ||
|
||
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, | ||
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); | ||
|
||
// Strictly typed sessionData | ||
type SessionData = Join<keyof NonNullable<Session["data"]>>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}, | ||
}, | ||
}) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.