From f9c6eb7f32a8ba18c1896e7f7b71102dd6289665 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Mon, 16 May 2022 21:40:55 -0400 Subject: [PATCH 01/10] Initial commit Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/README.md | 79 ++++++++ packages/adapter-chiselstrike/logo.svg | 9 + packages/adapter-chiselstrike/package.json | 47 +++++ packages/adapter-chiselstrike/src/index.ts | 191 ++++++++++++++++++ .../adapter-chiselstrike/tests/basic.test.ts | 48 +++++ .../adapter-chiselstrike/tests/custom.test.ts | 134 ++++++++++++ 6 files changed, 508 insertions(+) create mode 100644 packages/adapter-chiselstrike/README.md create mode 100644 packages/adapter-chiselstrike/logo.svg create mode 100644 packages/adapter-chiselstrike/package.json create mode 100644 packages/adapter-chiselstrike/src/index.ts create mode 100644 packages/adapter-chiselstrike/tests/basic.test.ts create mode 100644 packages/adapter-chiselstrike/tests/custom.test.ts diff --git a/packages/adapter-chiselstrike/README.md b/packages/adapter-chiselstrike/README.md new file mode 100644 index 0000000000..13ad861d38 --- /dev/null +++ b/packages/adapter-chiselstrike/README.md @@ -0,0 +1,79 @@ +

+
+      +

ChiselStrike Adapter - NextAuth.js

+

+ Open Source. Full Stack. Own Your Data. +

+

+ Build Test + Bundle Size + @next-auth/chiselstrike-adapter Version +

+

+ +## Overview + +This is the ChiselStrike Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary next-auth package. It is not a standalone package. + +## Getting Started + +Install `next-auth` and `@next-auth/chiselstrike-adapter` in your project: +```js +npm install next-auth @next-auth/chiselstrike-adapter +``` + +Configure NextAuth with the ChiselStrike adapter and a session callback to record the user ID. In +`pages/api/auth/[...nextauth].js`: +```js +const adapter = new CSAdapter(, ) + +export default NextAuth({ + adapter, + /// ... + callbacks: { + async session({ session, token, user }) { + session.userId = user.id + return session + } + } +}) + +``` + +When accessing ChiselStrike endpoints, you can provide the user ID value in a `ChiselUID` header. This is how +ChiselStrike knows which user is logged into the current session. For example: + +```js +await fetch('//', { headers: { "ChiselUID": session.userId } }) +``` + +## Contributing + +Initial setup: +```bash +git clone git@github.com:nextauthjs/next-auth.git +cd next-auth +pnpm i +pnpm build +cd packages/adapter-chiselstrike +pnpm i +``` + +Before running a build/test cycle, please set up a ChiselStrike backend, either locally or in the cloud. If locally, please create/edit a `.env` file in the directory where `chiseld` runs and put the following line in it: +```json +{ "CHISELD_AUTH_SECRET" : "1234" } +``` +If running a ChiselStrike backend in the cloud, please edit all instances of `new ChiselStrikeAdapter` in the `tests/` directory to reflect your backend's URL and auth secret. + +Build and test: +```bash +cd next-auth/packages/adapter-chiselstrike/ +pnpm test -- tests/basic.test.ts +pnpm test -- tests/custom.test.ts +``` +(Unfortunately, the custom tests interfere with basic tests if run in parallel, so we run them separately.) + +## License + +ISC diff --git a/packages/adapter-chiselstrike/logo.svg b/packages/adapter-chiselstrike/logo.svg new file mode 100644 index 0000000000..122ad76256 --- /dev/null +++ b/packages/adapter-chiselstrike/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/adapter-chiselstrike/package.json b/packages/adapter-chiselstrike/package.json new file mode 100644 index 0000000000..5ed3f96720 --- /dev/null +++ b/packages/adapter-chiselstrike/package.json @@ -0,0 +1,47 @@ +{ + "name": "@next-auth/chiselstrike-adapter", + "version": "1.0.0", + "description": "ChiselStrike adapter for next-auth.", + "homepage": "https://next-auth.js.org", + "repository": "https://github.com/nextauthjs/next-auth", + "bugs": { + "url": "https://github.com/nextauthjs/next-auth/issues" + }, + "author": "Dejan Mircevski", + "main": "dist/index.js", + "license": "ISC", + "keywords": [ + "next-auth", + "next.js", + "oauth", + "chiselstrike" + ], + "private": false, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "jest" + }, + "files": [ + "README.md", + "dist" + ], + "dependencies": { + "cross-fetch": "^3.1.5", + "next-auth": "workspace:" + }, + "peerDependencies": { + "cross-fetch": "^3.1.5", + "next-auth": "workspace:" + }, + "devDependencies": { + "@next-auth/adapter-test": "workspace:^0.0.0", + "@next-auth/tsconfig": "workspace:^0.0.0", + "jest": "^27.4.3", + "@types/jest": "^26.0.24" + }, + "jest": { + "preset": "@next-auth/adapter-test/jest" + } +} diff --git a/packages/adapter-chiselstrike/src/index.ts b/packages/adapter-chiselstrike/src/index.ts new file mode 100644 index 0000000000..7a90ca8b03 --- /dev/null +++ b/packages/adapter-chiselstrike/src/index.ts @@ -0,0 +1,191 @@ +import { fetch, Headers } from "cross-fetch"; +import { Account } from "next-auth"; +import type { Adapter, AdapterSession, AdapterUser, VerificationToken } from "next-auth/adapters" + +export class ChiselStrikeAdapter implements Adapter { + url: string; /// ChiselStrike backend. + secret: string; + + /// Makes an adapter connected to the ChiselStrike backend at this URL. Use the secret provided by the ChiselStrike platform. + constructor(url: string, secret: string) { + this.url = url + this.secret = secret + } + + /// Returns a ChiselStrike server auth path with this suffix. + private auth(suffix: string): string { + return `${this.url}/__chiselstrike/auth/${suffix}` + } + + public sessions(suffix?: string): string { + return this.auth('sessions') + (suffix ?? ''); + } + + public accounts(suffix?: string): string { + return this.auth('accounts') + (suffix ?? ''); + } + + public users(suffix?: string): string { + return this.auth('users') + (suffix ?? ''); + } + + public tokens(suffix?: string): string { + return this.auth('tokens') + (suffix ?? ''); + } + + /// Like fetch(), but adds a header with this.password. + public async secFetch(input: RequestInfo, init?: RequestInit | undefined): Promise { + init ??= {} + init.headers = new Headers(init.headers) + init.headers.set('ChiselAuth', this.secret) + return fetch(input, init) + } + + createUser = async (user: Omit): Promise => { + const resp = await this.secFetch(this.users(), { method: 'POST', body: JSON.stringify(user) }) + await ensureOK(resp, `posting user ${JSON.stringify(user)}`); + return userFromJson(await resp.json()) + } + + getUser = async (id: string): Promise => { + const resp = await this.secFetch(this.users(`/${id}`)) + if (!resp.ok) { return null } + return userFromJson(await resp.json()) + } + + getUserByEmail = async (email: string): Promise => { + return userFromJson(await firstElementOrNull(await this.secFetch(this.users(`?${makeFilter({ email })}`)))) + } + + updateUser = async (user: Partial): Promise => { + // TODO: use PATCH + const get = await this.secFetch(this.users(`/${user.id}`)) + await ensureOK(get, `getting user ${user.id}`) + let u = await get.json() + Object.keys(user).forEach(key => u[key] = user[key]) + const body = JSON.stringify(u) + const put = await this.secFetch(this.users(`/${user.id}`), { method: 'PUT', body }) + await ensureOK(put, `writing user ${body}`) + return userFromJson(u) + } + + getUserByAccount = async (providerAccountId: Pick): Promise => { + const acct = await firstElementOrNull(await this.secFetch(this.accounts(`?${makeFilter(providerAccountId)}`))) + if (!acct) { return null } + const uresp = await this.secFetch(this.users(`/${acct.userId}`)) + await ensureOK(uresp, `getting user ${acct.userId}`) + return userFromJson(await uresp.json()) + } + + // deleteUser?: ((userId: string) => Promise | Awaitable) | undefined; + deleteUser = async (userId: string): Promise => { + let resp = await this.secFetch(this.users(`/${userId}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting user ${userId}`) + resp = await this.secFetch(this.sessions(`?.userId=${userId}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting sessions for ${userId}`) + resp = await this.secFetch(this.accounts(`?.userId=${userId}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting accounts for ${userId}`) + return null + } + + // linkAccount: (account: Account) => Promise | Awaitable; + linkAccount = async (account: Account): Promise => { + const body = JSON.stringify(account); + const resp = await this.secFetch(this.accounts(), { method: 'POST', body }) + await ensureOK(resp, `posting account ${body}`); + return await resp.json() + } + + // unlinkAccount?: ((providerAccountId: Pick) => Promise | Awaitable) | undefined; + unlinkAccount = async (providerAccountId: Pick): Promise => { + const resp = await this.secFetch(this.accounts(`?${makeFilter(providerAccountId)}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting account ${JSON.stringify(providerAccountId)}`) + } + + // createSession: (session: { sessionToken: string; userId: string; expires: Date; }) => Awaitable; + createSession = async (session: { sessionToken: string; userId: string; expires: Date; }): Promise => { + const str = JSON.stringify(session); + const resp = await this.secFetch(this.sessions(), { method: 'POST', body: str }) + await ensureOK(resp, `posting session ${JSON.stringify(session)}`); + return sessionFromJson(await resp.json()) + } + + // getSessionAndUser: (sessionToken: string) => Awaitable<{ session: AdapterSession; user: AdapterUser; } | null>; + getSessionAndUser = async (sessionToken: string): Promise<{ session: AdapterSession; user: AdapterUser; } | null> => { + const session = await firstElementOrNull(await this.secFetch(this.sessions(`?.sessionToken=${sessionToken}`))) + if (!session) { return null } + const rUser = await this.secFetch(`${this.users()}/${session.userId}`) + await ensureOK(rUser, `fetching user ${session.userId}`) + return { session: sessionFromJson(session), user: userFromJson(await rUser.json()) } + } + + // updateSession: (session: Partial & Pick) => Awaitable; + updateSession = async (session: Partial & Pick): Promise => { + // TODO: use PATCH + const dbSession = await firstElementOrNull(await this.secFetch(this.sessions(`?.sessionToken=${session.sessionToken}`))) + if (!dbSession) { return null } + Object.keys(session).forEach(key => dbSession[key] = (session as Record)[key]) + const body = JSON.stringify(dbSession) + const put = await this.secFetch(this.sessions(`/${dbSession.id}`), { method: 'PUT', body }) + await ensureOK(put, `writing user ${body}`) + return dbSession + } + + // deleteSession: (sessionToken: string) => Promise | Awaitable; + deleteSession = async (sessionToken: string): Promise => { + const get = await this.secFetch(this.sessions(`?${makeFilter({ sessionToken })}`), { method: 'DELETE' }) + await ensureOK(get, `getting session with token ${sessionToken}`) + } + + // createVerificationToken?: ((verificationToken: VerificationToken) => Awaitable) | undefined; + createVerificationToken = async (verificationToken: VerificationToken): Promise => { + const str = JSON.stringify(verificationToken); + const resp = await this.secFetch(this.tokens(), { method: 'POST', body: str }); + await ensureOK(resp, `posting token ${str}`) + const token = await resp.json() + return { identifier: token.identifier, token: token.token, expires: new Date(token.expires) } + } + + // useVerificationToken?: ((params: { identifier: string; token: string; }) => Awaitable) | undefined; + useVerificationToken = async (params: { identifier: string; token: string; }): Promise => { + const token = await firstElementOrNull(await this.secFetch(this.tokens(`?${makeFilter(params)}`))) + if (!token) { return null } + await this.secFetch(this.tokens(`/${token.id}`), { method: 'DELETE' }) + return { identifier: token.identifier, token: token.token, expires: new Date(token.expires) } + } + + async deleteEverything() { + await this.secFetch(this.sessions('?all=true'), { method: 'DELETE' }) + await this.secFetch(this.accounts('?all=true'), { method: 'DELETE' }) + await this.secFetch(this.users('?all=true'), { method: 'DELETE' }) + await this.secFetch(this.tokens('?all=true'), { method: 'DELETE' }) + } +} + +function userFromJson(user: any): AdapterUser { + return user ? { ...user, emailVerified: new Date(user.emailVerified) } : null +} + +async function ensureOK(resp: Response, during: string) { + if (!resp.ok) { + const msg = `Error ${resp.status} during ${during}: ${await resp.text()}`; + console.error(msg); + throw msg; + } +} + +async function firstElementOrNull(resp: Response) { + if (!resp.ok) { return null } + const j = await resp.json() + if (!Array.isArray(j) || j.length < 1) { return null } + return j[0] +} + +function sessionFromJson(session: any): AdapterSession { + return { ...session, expires: new Date(session.expires), id: session.id ?? 'impossible: fetched CSession has null ID' } +} + +function makeFilter(propertyValues: Record) { + let filters = Object.entries(propertyValues).map(e => `.${e[0]}=${e[1]}`) + return filters.join('&') +} \ No newline at end of file diff --git a/packages/adapter-chiselstrike/tests/basic.test.ts b/packages/adapter-chiselstrike/tests/basic.test.ts new file mode 100644 index 0000000000..34eacc18d2 --- /dev/null +++ b/packages/adapter-chiselstrike/tests/basic.test.ts @@ -0,0 +1,48 @@ +import { runBasicTests } from "@next-auth/adapter-test" +import { ChiselStrikeAdapter } from "../src" +import fetch from "cross-fetch" + +const adapter = new ChiselStrikeAdapter('http://localhost:8080', '1234'); + +runBasicTests( + { + adapter, + db: { + connect: async () => { + await adapter.deleteEverything() + }, + session: async (sessionToken: string) => { + const s = await adFetch(adapter.sessions(`?.sessionToken=${sessionToken}`), `session ${sessionToken}`) + return s ? { ...s, expires: new Date(s.expires) } : null + }, + user: async (id: string) => { + return await adapter.getUser(id) + }, + account: async (providerAccountId: { provider: string; providerAccountId: string }) => { + const providerFilter = providerAccountId.provider ? `.provider=${providerAccountId.provider}` : '' + const providerAccountIdFilter = + providerAccountId.providerAccountId ? `.providerAccountId=${providerAccountId.providerAccountId}` : '' + return await adFetch( + adapter.accounts(`?${providerFilter}&${providerAccountIdFilter}`), + `account ${JSON.stringify(providerAccountId)}`) + }, + verificationToken: async (params: { identifier: string; token: string }) => { + const idFilter = `.identifier=${params.identifier}` + const tokenFilter = `.token=${params.token}` + let token = await adFetch( + adapter.tokens(`?${idFilter}&${tokenFilter}`), + `Fetching token ${JSON.stringify(params)}`) + return token ? { ...token, expires: new Date(token.expires), id: undefined } : null + } + } + } +) + +/// Fetches an object via adapter, performing common error-checking. +async function adFetch(url: string, what: string): Promise { + const res = await adapter.secFetch(url); + if (!res.ok) { return null } + const jres = await res.json(); + if (!Array.isArray(jres)) { throw new Error(`Fetch result for ${what}: ${JSON.stringify(jres)}`); } + return jres.length < 1 ? null : jres[0]; +} \ No newline at end of file diff --git a/packages/adapter-chiselstrike/tests/custom.test.ts b/packages/adapter-chiselstrike/tests/custom.test.ts new file mode 100644 index 0000000000..3198fabfb5 --- /dev/null +++ b/packages/adapter-chiselstrike/tests/custom.test.ts @@ -0,0 +1,134 @@ +/** @file + * Unit tests for the ChiselStrike adapter for NextAuth.js. To run, first create a .env file + * with the following contents: + * + * { "CHISELD_AUTH_SECRET" : "1234" } + * + * Start `chiseld` in the directory of that .env file, then run `jest` here in this directory. + */ + +import fetch from "cross-fetch" +import { Account } from "next-auth" +import { ChiselStrikeAdapter } from "../src" +export { } + +const a = new ChiselStrikeAdapter('http://localhost:8080', '1234') + +beforeEach(async () => { + await a.deleteEverything() +}) + +test('createUser returns payload', async () => { + const uMinimal = await a.createUser({ emailVerified: '2022-03-01' }) + expect(uMinimal).not.toBeNull() + expect(uMinimal?.emailVerified).toStrictEqual(new Date('2022-03-01')) + + const u2 = await a.createUser({ emailVerified: '2022-03-02', name: 'Bono', email: 'a@b.co', image: '' }) + expect(u2).not.toBeNull() + expect(u2?.emailVerified).toStrictEqual(new Date('2022-03-02')) + expect(u2?.name).toBe('Bono') + expect(u2?.email).toBe('a@b.co') + expect(u2?.image).toBe('') +}) + +test('getUser gets the right user', async () => { + const created = await a.createUser({ emailVerified: '2022-03-01', name: 'Johnny' }) + expect(created).not.toBeNull() + if (!created) return + const obtained = await a.getUser(created.id) + expect(obtained).toStrictEqual(created) +}) + +test('getUser returns null for non-existent ID', async () => { + expect(await a.getUser('never-going-to-exist-because-this-is-just-a-test')).toBeNull() +}) + +test('getUserByEmail gets the right user', async () => { + const email = 'dejan@chiselstrike.com' + const u1 = await a.createUser({ emailVerified: '2022-03-03', email, name: 'Heimdall' }) + expect(u1).not.toBeNull() + const u2 = await a.createUser({ emailVerified: '2022-03-03', email: '', name: 'Heimdall' }); + expect(u2).not.toBeNull() + expect(await a.getUserByEmail(email)).toStrictEqual(u1) + expect(await a.getUserByEmail('')).toStrictEqual(u2) +}) + +test('getUserByEmail returns null for non-existing email', async () => { + expect(await a.getUserByEmail('some absolutely non-existing email')).toBeNull() +}) + +test('updateUser updates correctly', async () => { + const id = (await a.createUser({ emailVerified: '2022-03-03', name: 'Popeye' }))?.id + const res = await a.updateUser({ id, name: 'Bluto', image: 'jpg' }) + expect(res).toStrictEqual({ name: 'Bluto', image: 'jpg', emailVerified: new Date('2022-03-03'), id }) + if (!id) { return } + const get = await a.getUser(id) + expect(get).toStrictEqual(res) +}) + +test('updateUser rejects non-existing id', async () => { + await expect(a.updateUser({ id: 'something completely unexpected and unique', email: 'rhododendron' })).rejects.toMatch('completely unexpected') +}) + +test('linkAccount results in getUserByAccount finding the right account', async () => { + const ghUser = await a.createUser({ emailVerified: '2022-03-03', name: 'GHUser' }) + await a.linkAccount({ provider: 'github', providerAccountId: '1234', userId: ghUser.id, type: 'oauth' }) + const twUser = await a.createUser({ emailVerified: '2022-03-03', name: 'TwUser' }) + await a.linkAccount({ provider: 'twitter', providerAccountId: '4321', userId: twUser.id, type: 'oauth' }) + expect(await a.getUserByAccount({ provider: 'github', providerAccountId: '1234' })).toStrictEqual(ghUser) + expect(await a.getUserByAccount({ provider: 'twitter', providerAccountId: '4321' })).toStrictEqual(twUser) +}) + +test('getUserByAccount returns null on mismatch', async () => { + expect(await a.getUserByAccount({ providerAccountId: 'impossible to exist', provider: 'fffff' })).toBeNull() +}) + +test('user deletion works', async () => { + const u = await a.createUser({ emailVerified: '2022-03-04', name: 'Tinker Bell' }) + expect(await a.getUser(u.id)).toStrictEqual(u) + expect(await a.deleteUser(u.id)).toBeNull() + expect(await a.getUser(u.id)).toBeNull() +}) + +test('unlinkAccount makes getUserByAccount not find the account (but getUser still finds the user)', async () => { + const u = await a.createUser({ emailVerified: '2022-03-03', name: 'Popeye' }) + const acct = { provider: 'github', providerAccountId: '1234', userId: u.id, type: 'oauth' } as Account + await a.linkAccount(acct) + expect(await a.getUserByAccount({ provider: 'github', providerAccountId: '1234' })).toStrictEqual(u) + await a.unlinkAccount(acct) + expect(await a.getUserByAccount({ provider: 'github', providerAccountId: '1234' })).toBeNull() + expect(await a.getUser(u.id)).toStrictEqual(u) +}) + +test('createSession makes getSessionAndUser find the created session', async () => { + const user = await a.createUser({ emailVerified: '2022-03-07', name: 'Oscar' }) + await a.createSession({ sessionToken: 'dummy', expires: new Date, userId: user.id }) + const session = await a.createSession({ sessionToken: 'token1', expires: new Date, userId: user.id }) + expect(await a.getSessionAndUser('token1')).toStrictEqual({ user, session }) +}) + +test('updateSession updates correctly', async () => { + const user = await a.createUser({ emailVerified: '2022-03-07', name: 'Oscar' }) + const s1 = await a.createSession({ sessionToken: 's1', expires: new Date, userId: user.id }) + const s2 = await a.createSession({ sessionToken: 's2', expires: new Date(10000), userId: user.id }) + await a.updateSession({ sessionToken: 's2', expires: new Date(50000) }) + expect(await a.getSessionAndUser('s2')).toStrictEqual({ user, session: { ...s2, expires: new Date(50000) } }) + expect(await a.getSessionAndUser('s1')).toStrictEqual({ user, session: s1 }) +}) + +test('deleteSession deletes the right session', async () => { + const user = await a.createUser({ emailVerified: '2022-03-07', name: 'Oscar' }) + const s1 = await a.createSession({ sessionToken: 's1', expires: new Date, userId: user.id }) + const s2 = await a.createSession({ sessionToken: 's2', expires: new Date, userId: user.id }) + await a.deleteSession('s1') + expect(await a.getSessionAndUser('s1')).toBeNull() + expect(await a.getSessionAndUser('s2')).toStrictEqual({ user, session: s2 }) +}) + +test('useVerificationToken finds token, deletes it', async () => { + const user = await a.createUser({ emailVerified: '2022-03-07', name: 'Oscar' }) + await a.createVerificationToken({ expires: new Date('2022-03-10'), identifier: '321', token: 'dummy' }) + const token = await a.createVerificationToken({ expires: new Date('2022-03-10'), identifier: '123', token: 'abc' }) + expect(await a.useVerificationToken({ identifier: '123', token: 'abc' })).toStrictEqual(token) + expect(await a.useVerificationToken({ identifier: '123', token: 'abc' })).toBeNull() +}) From 03fd7a57acaa838ca0418ae201488b12e25ea3b5 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Thu, 19 May 2022 20:44:03 -0400 Subject: [PATCH 02/10] Make ChiselStrikeAdapter a function https://github.com/nextauthjs/next-auth/pull/4573#pullrequestreview-978280321 Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/README.md | 4 +- packages/adapter-chiselstrike/src/index.ts | 243 +++++++++--------- .../adapter-chiselstrike/tests/basic.test.ts | 30 +-- .../adapter-chiselstrike/tests/custom.test.ts | 7 +- 4 files changed, 134 insertions(+), 150 deletions(-) diff --git a/packages/adapter-chiselstrike/README.md b/packages/adapter-chiselstrike/README.md index 13ad861d38..498d07a072 100644 --- a/packages/adapter-chiselstrike/README.md +++ b/packages/adapter-chiselstrike/README.md @@ -26,7 +26,7 @@ npm install next-auth @next-auth/chiselstrike-adapter Configure NextAuth with the ChiselStrike adapter and a session callback to record the user ID. In `pages/api/auth/[...nextauth].js`: ```js -const adapter = new CSAdapter(, ) +const adapter = ChiselStrikeAdapter(new ChiselStrikeAuthFetcher(, )) export default NextAuth({ adapter, @@ -64,7 +64,7 @@ Before running a build/test cycle, please set up a ChiselStrike backend, either ```json { "CHISELD_AUTH_SECRET" : "1234" } ``` -If running a ChiselStrike backend in the cloud, please edit all instances of `new ChiselStrikeAdapter` in the `tests/` directory to reflect your backend's URL and auth secret. +If running a ChiselStrike backend in the cloud, please edit all instances of `new ChiselStrikeAuthFetcher` in the `tests/` directory to reflect your backend's URL and auth secret. Build and test: ```bash diff --git a/packages/adapter-chiselstrike/src/index.ts b/packages/adapter-chiselstrike/src/index.ts index 7a90ca8b03..3128840c8a 100644 --- a/packages/adapter-chiselstrike/src/index.ts +++ b/packages/adapter-chiselstrike/src/index.ts @@ -2,11 +2,11 @@ import { fetch, Headers } from "cross-fetch"; import { Account } from "next-auth"; import type { Adapter, AdapterSession, AdapterUser, VerificationToken } from "next-auth/adapters" -export class ChiselStrikeAdapter implements Adapter { +export class ChiselStrikeAuthFetcher { url: string; /// ChiselStrike backend. secret: string; - /// Makes an adapter connected to the ChiselStrike backend at this URL. Use the secret provided by the ChiselStrike platform. + /// Makes a fetcher pointing to the ChiselStrike backend at this URL. Use the secret provided by the ChiselStrike platform. constructor(url: string, secret: string) { this.url = url this.secret = secret @@ -33,132 +33,132 @@ export class ChiselStrikeAdapter implements Adapter { return this.auth('tokens') + (suffix ?? ''); } - /// Like fetch(), but adds a header with this.password. - public async secFetch(input: RequestInfo, init?: RequestInit | undefined): Promise { + /// Like regular fetch(), but adds a header with this.secret. + public async fetch(input: RequestInfo, init?: RequestInit | undefined): Promise { init ??= {} init.headers = new Headers(init.headers) init.headers.set('ChiselAuth', this.secret) return fetch(input, init) } - createUser = async (user: Omit): Promise => { - const resp = await this.secFetch(this.users(), { method: 'POST', body: JSON.stringify(user) }) - await ensureOK(resp, `posting user ${JSON.stringify(user)}`); - return userFromJson(await resp.json()) + /// Fetches url and returns the first element of the resulting array (or null if anything goes wrong). + public async filter(url: string): Promise { + const res = await this.fetch(url); + if (!res.ok) { return null } + const jres = await res.json() + if (!Array.isArray(jres) || jres.length < 1) { return null } + return jres[0] } - getUser = async (id: string): Promise => { - const resp = await this.secFetch(this.users(`/${id}`)) - if (!resp.ok) { return null } - return userFromJson(await resp.json()) - } - - getUserByEmail = async (email: string): Promise => { - return userFromJson(await firstElementOrNull(await this.secFetch(this.users(`?${makeFilter({ email })}`)))) - } - - updateUser = async (user: Partial): Promise => { - // TODO: use PATCH - const get = await this.secFetch(this.users(`/${user.id}`)) - await ensureOK(get, `getting user ${user.id}`) - let u = await get.json() - Object.keys(user).forEach(key => u[key] = user[key]) - const body = JSON.stringify(u) - const put = await this.secFetch(this.users(`/${user.id}`), { method: 'PUT', body }) - await ensureOK(put, `writing user ${body}`) - return userFromJson(u) - } - - getUserByAccount = async (providerAccountId: Pick): Promise => { - const acct = await firstElementOrNull(await this.secFetch(this.accounts(`?${makeFilter(providerAccountId)}`))) - if (!acct) { return null } - const uresp = await this.secFetch(this.users(`/${acct.userId}`)) - await ensureOK(uresp, `getting user ${acct.userId}`) - return userFromJson(await uresp.json()) - } - - // deleteUser?: ((userId: string) => Promise | Awaitable) | undefined; - deleteUser = async (userId: string): Promise => { - let resp = await this.secFetch(this.users(`/${userId}`), { method: 'DELETE' }) - await ensureOK(resp, `deleting user ${userId}`) - resp = await this.secFetch(this.sessions(`?.userId=${userId}`), { method: 'DELETE' }) - await ensureOK(resp, `deleting sessions for ${userId}`) - resp = await this.secFetch(this.accounts(`?.userId=${userId}`), { method: 'DELETE' }) - await ensureOK(resp, `deleting accounts for ${userId}`) - return null - } - - // linkAccount: (account: Account) => Promise | Awaitable; - linkAccount = async (account: Account): Promise => { - const body = JSON.stringify(account); - const resp = await this.secFetch(this.accounts(), { method: 'POST', body }) - await ensureOK(resp, `posting account ${body}`); - return await resp.json() - } - - // unlinkAccount?: ((providerAccountId: Pick) => Promise | Awaitable) | undefined; - unlinkAccount = async (providerAccountId: Pick): Promise => { - const resp = await this.secFetch(this.accounts(`?${makeFilter(providerAccountId)}`), { method: 'DELETE' }) - await ensureOK(resp, `deleting account ${JSON.stringify(providerAccountId)}`) - } - - // createSession: (session: { sessionToken: string; userId: string; expires: Date; }) => Awaitable; - createSession = async (session: { sessionToken: string; userId: string; expires: Date; }): Promise => { - const str = JSON.stringify(session); - const resp = await this.secFetch(this.sessions(), { method: 'POST', body: str }) - await ensureOK(resp, `posting session ${JSON.stringify(session)}`); - return sessionFromJson(await resp.json()) - } - - // getSessionAndUser: (sessionToken: string) => Awaitable<{ session: AdapterSession; user: AdapterUser; } | null>; - getSessionAndUser = async (sessionToken: string): Promise<{ session: AdapterSession; user: AdapterUser; } | null> => { - const session = await firstElementOrNull(await this.secFetch(this.sessions(`?.sessionToken=${sessionToken}`))) - if (!session) { return null } - const rUser = await this.secFetch(`${this.users()}/${session.userId}`) - await ensureOK(rUser, `fetching user ${session.userId}`) - return { session: sessionFromJson(session), user: userFromJson(await rUser.json()) } - } - - // updateSession: (session: Partial & Pick) => Awaitable; - updateSession = async (session: Partial & Pick): Promise => { - // TODO: use PATCH - const dbSession = await firstElementOrNull(await this.secFetch(this.sessions(`?.sessionToken=${session.sessionToken}`))) - if (!dbSession) { return null } - Object.keys(session).forEach(key => dbSession[key] = (session as Record)[key]) - const body = JSON.stringify(dbSession) - const put = await this.secFetch(this.sessions(`/${dbSession.id}`), { method: 'PUT', body }) - await ensureOK(put, `writing user ${body}`) - return dbSession - } - - // deleteSession: (sessionToken: string) => Promise | Awaitable; - deleteSession = async (sessionToken: string): Promise => { - const get = await this.secFetch(this.sessions(`?${makeFilter({ sessionToken })}`), { method: 'DELETE' }) - await ensureOK(get, `getting session with token ${sessionToken}`) - } - - // createVerificationToken?: ((verificationToken: VerificationToken) => Awaitable) | undefined; - createVerificationToken = async (verificationToken: VerificationToken): Promise => { - const str = JSON.stringify(verificationToken); - const resp = await this.secFetch(this.tokens(), { method: 'POST', body: str }); - await ensureOK(resp, `posting token ${str}`) - const token = await resp.json() - return { identifier: token.identifier, token: token.token, expires: new Date(token.expires) } - } - - // useVerificationToken?: ((params: { identifier: string; token: string; }) => Awaitable) | undefined; - useVerificationToken = async (params: { identifier: string; token: string; }): Promise => { - const token = await firstElementOrNull(await this.secFetch(this.tokens(`?${makeFilter(params)}`))) - if (!token) { return null } - await this.secFetch(this.tokens(`/${token.id}`), { method: 'DELETE' }) - return { identifier: token.identifier, token: token.token, expires: new Date(token.expires) } + public async deleteEverything() { + await this.fetch(this.sessions('?all=true'), { method: 'DELETE' }) + await this.fetch(this.accounts('?all=true'), { method: 'DELETE' }) + await this.fetch(this.users('?all=true'), { method: 'DELETE' }) + await this.fetch(this.tokens('?all=true'), { method: 'DELETE' }) } +} - async deleteEverything() { - await this.secFetch(this.sessions('?all=true'), { method: 'DELETE' }) - await this.secFetch(this.accounts('?all=true'), { method: 'DELETE' }) - await this.secFetch(this.users('?all=true'), { method: 'DELETE' }) - await this.secFetch(this.tokens('?all=true'), { method: 'DELETE' }) +export function ChiselStrikeAdapter(fetcher: ChiselStrikeAuthFetcher): Adapter { + return { + createUser: async (user: Omit): Promise => { + const resp = await fetcher.fetch(fetcher.users(), { method: 'POST', body: JSON.stringify(user) }) + await ensureOK(resp, `posting user ${JSON.stringify(user)}`); + return userFromJson(await resp.json()) + }, + getUser: async (id: string): Promise => { + const resp = await fetcher.fetch(fetcher.users(`/${id}`)) + if (!resp.ok) { return null } + return userFromJson(await resp.json()) + }, + getUserByEmail: async (email: string): Promise => { + return userFromJson(await fetcher.filter(fetcher.users(`?${makeFilter({ email })}`))) + }, + updateUser: async (user: Partial): Promise => { + // TODO: use PATCH + const get = await fetcher.fetch(fetcher.users(`/${user.id}`)) + await ensureOK(get, `getting user ${user.id}`) + let u = await get.json() + Object.keys(user).forEach(key => u[key] = user[key]) + const body = JSON.stringify(u) + const put = await fetcher.fetch(fetcher.users(`/${user.id}`), { method: 'PUT', body }) + await ensureOK(put, `writing user ${body}`) + return userFromJson(u) + }, + getUserByAccount: async (providerAccountId: Pick): Promise => { + const acct = await fetcher.filter(fetcher.accounts(`?${makeFilter(providerAccountId)}`)) + if (!acct) { return null } + const uresp = await fetcher.fetch(fetcher.users(`/${acct.userId}`)) + await ensureOK(uresp, `getting user ${acct.userId}`) + return userFromJson(await uresp.json()) + }, + // deleteUser?: ((userId: string) => Promise | Awaitable) | undefined; + deleteUser: async (userId: string): Promise => { + let resp = await fetcher.fetch(fetcher.users(`/${userId}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting user ${userId}`) + resp = await fetcher.fetch(fetcher.sessions(`?.userId=${userId}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting sessions for ${userId}`) + resp = await fetcher.fetch(fetcher.accounts(`?.userId=${userId}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting accounts for ${userId}`) + return null + }, + // linkAccount: (account: Account) => Promise | Awaitable; + linkAccount: async (account: Account): Promise => { + const body = JSON.stringify(account); + const resp = await fetcher.fetch(fetcher.accounts(), { method: 'POST', body }) + await ensureOK(resp, `posting account ${body}`); + return await resp.json() + }, + // unlinkAccount?: ((providerAccountId: Pick) => Promise | Awaitable) | undefined; + unlinkAccount: async (providerAccountId: Pick): Promise => { + const resp = await fetcher.fetch(fetcher.accounts(`?${makeFilter(providerAccountId)}`), { method: 'DELETE' }) + await ensureOK(resp, `deleting account ${JSON.stringify(providerAccountId)}`) + }, + // createSession: (session: { sessionToken: string; userId: string; expires: Date; }) => Awaitable; + createSession: async (session: { sessionToken: string; userId: string; expires: Date; }): Promise => { + const str = JSON.stringify(session); + const resp = await fetcher.fetch(fetcher.sessions(), { method: 'POST', body: str }) + await ensureOK(resp, `posting session ${JSON.stringify(session)}`); + return sessionFromJson(await resp.json()) + }, + // getSessionAndUser: (sessionToken: string) => Awaitable<{ session: AdapterSession; user: AdapterUser; } | null>; + getSessionAndUser: async (sessionToken: string): Promise<{ session: AdapterSession; user: AdapterUser; } | null> => { + const session = await fetcher.filter(fetcher.sessions(`?.sessionToken=${sessionToken}`)) + if (!session) { return null } + const rUser = await fetcher.fetch(fetcher.users(`/${session.userId}`)) + await ensureOK(rUser, `fetching user ${session.userId}`) + return { session: sessionFromJson(session), user: userFromJson(await rUser.json()) } + }, + // updateSession: (session: Partial & Pick) => Awaitable; + updateSession: async (session: Partial & Pick): Promise => { + // TODO: use PATCH + const dbSession = await fetcher.filter(fetcher.sessions(`?.sessionToken=${session.sessionToken}`)) + if (!dbSession) { return null } + Object.keys(session).forEach(key => dbSession[key] = session[key]) + const body = JSON.stringify(dbSession) + const put = await fetcher.fetch(fetcher.sessions(`/${dbSession.id}`), { method: 'PUT', body }) + await ensureOK(put, `writing user ${body}`) + return dbSession as unknown as AdapterSession + }, + // deleteSession: (sessionToken: string) => Promise | Awaitable; + deleteSession: async (sessionToken: string): Promise => { + const get = await fetcher.fetch(fetcher.sessions(`?${makeFilter({ sessionToken })}`), { method: 'DELETE' }) + await ensureOK(get, `getting session with token ${sessionToken}`) + }, + // createVerificationToken?: ((verificationToken: VerificationToken) => Awaitable) | undefined; + createVerificationToken: async (verificationToken: VerificationToken): Promise => { + const str = JSON.stringify(verificationToken); + const resp = await fetcher.fetch(fetcher.tokens(), { method: 'POST', body: str }); + await ensureOK(resp, `posting token ${str}`) + const token = await resp.json() + return { identifier: token.identifier, token: token.token, expires: new Date(token.expires) } + }, + // useVerificationToken?: ((params: { identifier: string; token: string; }) => Awaitable) | undefined; + useVerificationToken: async (params: { identifier: string; token: string; }): Promise => { + const token = await fetcher.filter(fetcher.tokens(`?${makeFilter(params)}`)) + if (!token) { return null } + await fetcher.fetch(fetcher.tokens(`/${token.id}`), { method: 'DELETE' }) + return { identifier: token.identifier, token: token.token, expires: new Date(token.expires) } + } } } @@ -174,13 +174,6 @@ async function ensureOK(resp: Response, during: string) { } } -async function firstElementOrNull(resp: Response) { - if (!resp.ok) { return null } - const j = await resp.json() - if (!Array.isArray(j) || j.length < 1) { return null } - return j[0] -} - function sessionFromJson(session: any): AdapterSession { return { ...session, expires: new Date(session.expires), id: session.id ?? 'impossible: fetched CSession has null ID' } } diff --git a/packages/adapter-chiselstrike/tests/basic.test.ts b/packages/adapter-chiselstrike/tests/basic.test.ts index 34eacc18d2..aa8c6ff5ee 100644 --- a/packages/adapter-chiselstrike/tests/basic.test.ts +++ b/packages/adapter-chiselstrike/tests/basic.test.ts @@ -1,18 +1,19 @@ import { runBasicTests } from "@next-auth/adapter-test" -import { ChiselStrikeAdapter } from "../src" +import { ChiselStrikeAdapter, ChiselStrikeAuthFetcher } from "../src" import fetch from "cross-fetch" -const adapter = new ChiselStrikeAdapter('http://localhost:8080', '1234'); +const fetcher = new ChiselStrikeAuthFetcher('http://localhost:8080', '1234') +const adapter = ChiselStrikeAdapter(fetcher) runBasicTests( { adapter, db: { connect: async () => { - await adapter.deleteEverything() + await fetcher.deleteEverything() }, session: async (sessionToken: string) => { - const s = await adFetch(adapter.sessions(`?.sessionToken=${sessionToken}`), `session ${sessionToken}`) + const s = await fetcher.filter(fetcher.sessions(`?.sessionToken=${sessionToken}`)) return s ? { ...s, expires: new Date(s.expires) } : null }, user: async (id: string) => { @@ -22,27 +23,16 @@ runBasicTests( const providerFilter = providerAccountId.provider ? `.provider=${providerAccountId.provider}` : '' const providerAccountIdFilter = providerAccountId.providerAccountId ? `.providerAccountId=${providerAccountId.providerAccountId}` : '' - return await adFetch( - adapter.accounts(`?${providerFilter}&${providerAccountIdFilter}`), - `account ${JSON.stringify(providerAccountId)}`) + return await fetcher.filter( + fetcher.accounts(`?${providerFilter}&${providerAccountIdFilter}`)) }, verificationToken: async (params: { identifier: string; token: string }) => { const idFilter = `.identifier=${params.identifier}` const tokenFilter = `.token=${params.token}` - let token = await adFetch( - adapter.tokens(`?${idFilter}&${tokenFilter}`), - `Fetching token ${JSON.stringify(params)}`) + let token = await fetcher.filter( + fetcher.tokens(`?${idFilter}&${tokenFilter}`)) return token ? { ...token, expires: new Date(token.expires), id: undefined } : null } } } -) - -/// Fetches an object via adapter, performing common error-checking. -async function adFetch(url: string, what: string): Promise { - const res = await adapter.secFetch(url); - if (!res.ok) { return null } - const jres = await res.json(); - if (!Array.isArray(jres)) { throw new Error(`Fetch result for ${what}: ${JSON.stringify(jres)}`); } - return jres.length < 1 ? null : jres[0]; -} \ No newline at end of file +) \ No newline at end of file diff --git a/packages/adapter-chiselstrike/tests/custom.test.ts b/packages/adapter-chiselstrike/tests/custom.test.ts index 3198fabfb5..1926d84df0 100644 --- a/packages/adapter-chiselstrike/tests/custom.test.ts +++ b/packages/adapter-chiselstrike/tests/custom.test.ts @@ -9,13 +9,14 @@ import fetch from "cross-fetch" import { Account } from "next-auth" -import { ChiselStrikeAdapter } from "../src" +import { ChiselStrikeAdapter, ChiselStrikeAuthFetcher } from "../src" export { } -const a = new ChiselStrikeAdapter('http://localhost:8080', '1234') +const fetcher = new ChiselStrikeAuthFetcher('http://localhost:8080', '1234') +const a = ChiselStrikeAdapter(fetcher) beforeEach(async () => { - await a.deleteEverything() + await fetcher.deleteEverything() }) test('createUser returns payload', async () => { From 5f089474d7a33dee463d08ad774c46ad6af59e78 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Thu, 19 May 2022 23:12:11 -0400 Subject: [PATCH 03/10] Use node-fetch instead of cross-fetch Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/package.json | 8 ++++---- packages/adapter-chiselstrike/src/index.ts | 3 ++- packages/adapter-chiselstrike/tests/basic.test.ts | 2 +- packages/adapter-chiselstrike/tests/custom.test.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/adapter-chiselstrike/package.json b/packages/adapter-chiselstrike/package.json index 5ed3f96720..96d9f46a12 100644 --- a/packages/adapter-chiselstrike/package.json +++ b/packages/adapter-chiselstrike/package.json @@ -28,12 +28,12 @@ "dist" ], "dependencies": { - "cross-fetch": "^3.1.5", - "next-auth": "workspace:" + "next-auth": "workspace:", + "node-fetch": "^2.6.1" }, "peerDependencies": { - "cross-fetch": "^3.1.5", - "next-auth": "workspace:" + "next-auth": "workspace:", + "node-fetch": "^2.6.1" }, "devDependencies": { "@next-auth/adapter-test": "workspace:^0.0.0", diff --git a/packages/adapter-chiselstrike/src/index.ts b/packages/adapter-chiselstrike/src/index.ts index 3128840c8a..f56d66d96b 100644 --- a/packages/adapter-chiselstrike/src/index.ts +++ b/packages/adapter-chiselstrike/src/index.ts @@ -1,6 +1,7 @@ -import { fetch, Headers } from "cross-fetch"; import { Account } from "next-auth"; import type { Adapter, AdapterSession, AdapterUser, VerificationToken } from "next-auth/adapters" +import { Headers, RequestInfo, RequestInit, Response } from "node-fetch" +import fetch from "node-fetch" export class ChiselStrikeAuthFetcher { url: string; /// ChiselStrike backend. diff --git a/packages/adapter-chiselstrike/tests/basic.test.ts b/packages/adapter-chiselstrike/tests/basic.test.ts index aa8c6ff5ee..5900fc009e 100644 --- a/packages/adapter-chiselstrike/tests/basic.test.ts +++ b/packages/adapter-chiselstrike/tests/basic.test.ts @@ -1,6 +1,6 @@ import { runBasicTests } from "@next-auth/adapter-test" import { ChiselStrikeAdapter, ChiselStrikeAuthFetcher } from "../src" -import fetch from "cross-fetch" +import fetch from "node-fetch" const fetcher = new ChiselStrikeAuthFetcher('http://localhost:8080', '1234') const adapter = ChiselStrikeAdapter(fetcher) diff --git a/packages/adapter-chiselstrike/tests/custom.test.ts b/packages/adapter-chiselstrike/tests/custom.test.ts index 1926d84df0..9b3ef7797d 100644 --- a/packages/adapter-chiselstrike/tests/custom.test.ts +++ b/packages/adapter-chiselstrike/tests/custom.test.ts @@ -7,7 +7,7 @@ * Start `chiseld` in the directory of that .env file, then run `jest` here in this directory. */ -import fetch from "cross-fetch" +import fetch from "node-fetch" import { Account } from "next-auth" import { ChiselStrikeAdapter, ChiselStrikeAuthFetcher } from "../src" export { } From 24a60b75f680aa40fa31bf9345ae9533a6f99d55 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Thu, 23 Jun 2022 10:20:57 -0400 Subject: [PATCH 04/10] Update for ChiselStrike v0.10 Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/package.json | 2 +- packages/adapter-chiselstrike/src/index.ts | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/adapter-chiselstrike/package.json b/packages/adapter-chiselstrike/package.json index 96d9f46a12..329c78c1ea 100644 --- a/packages/adapter-chiselstrike/package.json +++ b/packages/adapter-chiselstrike/package.json @@ -1,6 +1,6 @@ { "name": "@next-auth/chiselstrike-adapter", - "version": "1.0.0", + "version": "0.10.0", "description": "ChiselStrike adapter for next-auth.", "homepage": "https://next-auth.js.org", "repository": "https://github.com/nextauthjs/next-auth", diff --git a/packages/adapter-chiselstrike/src/index.ts b/packages/adapter-chiselstrike/src/index.ts index f56d66d96b..2b5368b910 100644 --- a/packages/adapter-chiselstrike/src/index.ts +++ b/packages/adapter-chiselstrike/src/index.ts @@ -44,11 +44,11 @@ export class ChiselStrikeAuthFetcher { /// Fetches url and returns the first element of the resulting array (or null if anything goes wrong). public async filter(url: string): Promise { - const res = await this.fetch(url); + const res = await this.fetch(url) if (!res.ok) { return null } const jres = await res.json() - if (!Array.isArray(jres) || jres.length < 1) { return null } - return jres[0] + if (!Array.isArray(jres.results) || jres.results.length < 1) { return null } + return jres.results[0] } public async deleteEverything() { @@ -107,7 +107,7 @@ export function ChiselStrikeAdapter(fetcher: ChiselStrikeAuthFetcher): Adapter { const body = JSON.stringify(account); const resp = await fetcher.fetch(fetcher.accounts(), { method: 'POST', body }) await ensureOK(resp, `posting account ${body}`); - return await resp.json() + return (await resp.json()).results }, // unlinkAccount?: ((providerAccountId: Pick) => Promise | Awaitable) | undefined; unlinkAccount: async (providerAccountId: Pick): Promise => { @@ -163,7 +163,8 @@ export function ChiselStrikeAdapter(fetcher: ChiselStrikeAuthFetcher): Adapter { } } -function userFromJson(user: any): AdapterUser { +function userFromJson(json: any): AdapterUser { + const user = json?.results ?? json return user ? { ...user, emailVerified: new Date(user.emailVerified) } : null } @@ -175,7 +176,8 @@ async function ensureOK(resp: Response, during: string) { } } -function sessionFromJson(session: any): AdapterSession { +function sessionFromJson(json: any): AdapterSession { + const session = json?.results ?? json return { ...session, expires: new Date(session.expires), id: session.id ?? 'impossible: fetched CSession has null ID' } } From ab20bf7984403f70d0b87929c90e9d7e79b8beb4 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Thu, 23 Jun 2022 10:25:50 -0400 Subject: [PATCH 05/10] Fix type error in updateSession Thanks @luisfvieirasilva ! Co-authored-by: luisfvieirasilva --- packages/adapter-chiselstrike/src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/adapter-chiselstrike/src/index.ts b/packages/adapter-chiselstrike/src/index.ts index 2b5368b910..4bfa10326a 100644 --- a/packages/adapter-chiselstrike/src/index.ts +++ b/packages/adapter-chiselstrike/src/index.ts @@ -134,7 +134,13 @@ export function ChiselStrikeAdapter(fetcher: ChiselStrikeAuthFetcher): Adapter { // TODO: use PATCH const dbSession = await fetcher.filter(fetcher.sessions(`?.sessionToken=${session.sessionToken}`)) if (!dbSession) { return null } - Object.keys(session).forEach(key => dbSession[key] = session[key]) + Object.entries(session).forEach(([key, entry]) => { + if (entry instanceof Date) { + dbSession[key] = entry.toISOString(); + } else { + dbSession[key] = entry; + } + }); const body = JSON.stringify(dbSession) const put = await fetcher.fetch(fetcher.sessions(`/${dbSession.id}`), { method: 'PUT', body }) await ensureOK(put, `writing user ${body}`) From 125ca13f71b663aa21ec7c42173fd4fb9daf0e7f Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Fri, 24 Jun 2022 11:37:40 -0400 Subject: [PATCH 06/10] Add tsc build Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/package.json | 1 + packages/adapter-chiselstrike/tsconfig.json | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 packages/adapter-chiselstrike/tsconfig.json diff --git a/packages/adapter-chiselstrike/package.json b/packages/adapter-chiselstrike/package.json index 329c78c1ea..4b54f8ec90 100644 --- a/packages/adapter-chiselstrike/package.json +++ b/packages/adapter-chiselstrike/package.json @@ -21,6 +21,7 @@ "access": "public" }, "scripts": { + "build": "tsc", "test": "jest" }, "files": [ diff --git a/packages/adapter-chiselstrike/tsconfig.json b/packages/adapter-chiselstrike/tsconfig.json new file mode 100644 index 0000000000..4a4889cf23 --- /dev/null +++ b/packages/adapter-chiselstrike/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@next-auth/tsconfig/adapters.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + }, + "include": ["."], + "exclude": ["tests", "dist", "jest.config.js"] +} From 42903e164ae159b093e1d284483679c1e3f53f05 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Fri, 24 Jun 2022 11:44:17 -0400 Subject: [PATCH 07/10] Remove fetch polyfill We now only use `node-fetch` in tests. Thanks to @barbieri for coding this. Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/jest.config.js | 5 +++++ packages/adapter-chiselstrike/package.json | 14 ++++---------- packages/adapter-chiselstrike/src/index.ts | 4 +--- packages/adapter-chiselstrike/tests/basic.test.ts | 1 - packages/adapter-chiselstrike/tests/custom.test.ts | 1 - packages/adapter-chiselstrike/tests/jest-setup.js | 3 +++ packages/adapter-chiselstrike/tsconfig.json | 3 +-- 7 files changed, 14 insertions(+), 17 deletions(-) create mode 100644 packages/adapter-chiselstrike/jest.config.js create mode 100644 packages/adapter-chiselstrike/tests/jest-setup.js diff --git a/packages/adapter-chiselstrike/jest.config.js b/packages/adapter-chiselstrike/jest.config.js new file mode 100644 index 0000000000..5b8f8fcc1e --- /dev/null +++ b/packages/adapter-chiselstrike/jest.config.js @@ -0,0 +1,5 @@ +/** @type { import('@jest/types').Config.InitialOptions } */ +module.exports = { + preset: "@next-auth/adapter-test/jest", + setupFilesAfterEnv: ["./tests/jest-setup.js"] +}; diff --git a/packages/adapter-chiselstrike/package.json b/packages/adapter-chiselstrike/package.json index 4b54f8ec90..87c25c5bb8 100644 --- a/packages/adapter-chiselstrike/package.json +++ b/packages/adapter-chiselstrike/package.json @@ -28,21 +28,15 @@ "README.md", "dist" ], - "dependencies": { - "next-auth": "workspace:", - "node-fetch": "^2.6.1" - }, "peerDependencies": { - "next-auth": "workspace:", - "node-fetch": "^2.6.1" + "next-auth": "workspace:*" }, "devDependencies": { "@next-auth/adapter-test": "workspace:^0.0.0", "@next-auth/tsconfig": "workspace:^0.0.0", + "@types/jest": "^26.0.24", "jest": "^27.4.3", - "@types/jest": "^26.0.24" - }, - "jest": { - "preset": "@next-auth/adapter-test/jest" + "next-auth": "workspace:*", + "node-fetch": "^2.6.1" } } diff --git a/packages/adapter-chiselstrike/src/index.ts b/packages/adapter-chiselstrike/src/index.ts index 4bfa10326a..72f049f4b3 100644 --- a/packages/adapter-chiselstrike/src/index.ts +++ b/packages/adapter-chiselstrike/src/index.ts @@ -1,7 +1,5 @@ -import { Account } from "next-auth"; +import type { Account } from "next-auth"; import type { Adapter, AdapterSession, AdapterUser, VerificationToken } from "next-auth/adapters" -import { Headers, RequestInfo, RequestInit, Response } from "node-fetch" -import fetch from "node-fetch" export class ChiselStrikeAuthFetcher { url: string; /// ChiselStrike backend. diff --git a/packages/adapter-chiselstrike/tests/basic.test.ts b/packages/adapter-chiselstrike/tests/basic.test.ts index 5900fc009e..d4b46b0042 100644 --- a/packages/adapter-chiselstrike/tests/basic.test.ts +++ b/packages/adapter-chiselstrike/tests/basic.test.ts @@ -1,6 +1,5 @@ import { runBasicTests } from "@next-auth/adapter-test" import { ChiselStrikeAdapter, ChiselStrikeAuthFetcher } from "../src" -import fetch from "node-fetch" const fetcher = new ChiselStrikeAuthFetcher('http://localhost:8080', '1234') const adapter = ChiselStrikeAdapter(fetcher) diff --git a/packages/adapter-chiselstrike/tests/custom.test.ts b/packages/adapter-chiselstrike/tests/custom.test.ts index 9b3ef7797d..4bc78d5ca5 100644 --- a/packages/adapter-chiselstrike/tests/custom.test.ts +++ b/packages/adapter-chiselstrike/tests/custom.test.ts @@ -7,7 +7,6 @@ * Start `chiseld` in the directory of that .env file, then run `jest` here in this directory. */ -import fetch from "node-fetch" import { Account } from "next-auth" import { ChiselStrikeAdapter, ChiselStrikeAuthFetcher } from "../src" export { } diff --git a/packages/adapter-chiselstrike/tests/jest-setup.js b/packages/adapter-chiselstrike/tests/jest-setup.js new file mode 100644 index 0000000000..96c9aa1a31 --- /dev/null +++ b/packages/adapter-chiselstrike/tests/jest-setup.js @@ -0,0 +1,3 @@ +import fetch from "node-fetch" +globalThis.fetch = fetch; +globalThis.Headers = fetch.Headers; diff --git a/packages/adapter-chiselstrike/tsconfig.json b/packages/adapter-chiselstrike/tsconfig.json index 4a4889cf23..bf0a7cabce 100644 --- a/packages/adapter-chiselstrike/tsconfig.json +++ b/packages/adapter-chiselstrike/tsconfig.json @@ -2,8 +2,7 @@ "extends": "@next-auth/tsconfig/adapters.json", "compilerOptions": { "rootDir": "src", - "outDir": "dist", + "outDir": "dist" }, - "include": ["."], "exclude": ["tests", "dist", "jest.config.js"] } From 98841a0c493cebd927f9daef676afdc1b555ecc6 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Mon, 27 Jun 2022 11:09:32 -0400 Subject: [PATCH 08/10] Use a specific next-auth version in peerDependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Balázs Orbán --- packages/adapter-chiselstrike/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-chiselstrike/package.json b/packages/adapter-chiselstrike/package.json index 87c25c5bb8..ef0f6d0d85 100644 --- a/packages/adapter-chiselstrike/package.json +++ b/packages/adapter-chiselstrike/package.json @@ -29,7 +29,7 @@ "dist" ], "peerDependencies": { - "next-auth": "workspace:*" + "next-auth": "^4.6.0" }, "devDependencies": { "@next-auth/adapter-test": "workspace:^0.0.0", From 58243aa3c3bdd31e2af0a5b608b66e9075069bfd Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Mon, 27 Jun 2022 12:06:32 -0400 Subject: [PATCH 09/10] Use Promise.all instead of four awaits Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/src/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/adapter-chiselstrike/src/index.ts b/packages/adapter-chiselstrike/src/index.ts index 72f049f4b3..503e3f4e1f 100644 --- a/packages/adapter-chiselstrike/src/index.ts +++ b/packages/adapter-chiselstrike/src/index.ts @@ -50,10 +50,12 @@ export class ChiselStrikeAuthFetcher { } public async deleteEverything() { - await this.fetch(this.sessions('?all=true'), { method: 'DELETE' }) - await this.fetch(this.accounts('?all=true'), { method: 'DELETE' }) - await this.fetch(this.users('?all=true'), { method: 'DELETE' }) - await this.fetch(this.tokens('?all=true'), { method: 'DELETE' }) + await Promise.all([ + this.fetch(this.sessions('?all=true'), { method: 'DELETE' }), + this.fetch(this.accounts('?all=true'), { method: 'DELETE' }), + this.fetch(this.users('?all=true'), { method: 'DELETE' }), + this.fetch(this.tokens('?all=true'), { method: 'DELETE' }) + ]) } } @@ -138,7 +140,7 @@ export function ChiselStrikeAdapter(fetcher: ChiselStrikeAuthFetcher): Adapter { } else { dbSession[key] = entry; } - }); + }); const body = JSON.stringify(dbSession) const put = await fetcher.fetch(fetcher.sessions(`/${dbSession.id}`), { method: 'PUT', body }) await ensureOK(put, `writing user ${body}`) From 9b00fcdcc3c0bda5a5e13dc24deba42706c2c969 Mon Sep 17 00:00:00 2001 From: Dejan Mircevski Date: Mon, 27 Jun 2022 13:00:02 -0400 Subject: [PATCH 10/10] Get DB user from the DB, not adapter Signed-off-by: Dejan Mircevski --- packages/adapter-chiselstrike/tests/basic.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/adapter-chiselstrike/tests/basic.test.ts b/packages/adapter-chiselstrike/tests/basic.test.ts index d4b46b0042..df21cf0f08 100644 --- a/packages/adapter-chiselstrike/tests/basic.test.ts +++ b/packages/adapter-chiselstrike/tests/basic.test.ts @@ -16,7 +16,10 @@ runBasicTests( return s ? { ...s, expires: new Date(s.expires) } : null }, user: async (id: string) => { - return await adapter.getUser(id) + const resp = await fetcher.fetch(fetcher.users(`/${id}`)) + if (!resp.ok) { return null } + const json = await resp.json() + return { ...json, emailVerified: new Date(json.emailVerified) } }, account: async (providerAccountId: { provider: string; providerAccountId: string }) => { const providerFilter = providerAccountId.provider ? `.provider=${providerAccountId.provider}` : ''