Skip to content

Add @auth/kinto-adapter #12524

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
48 changes: 48 additions & 0 deletions packages/adapter-kinto/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@auth/kinto-adapter",
"version": "2.7.4",
"description": "Kinto adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "Les De Ridder <[email protected]>",
"license": "ISC",
"keywords": [
"next-auth",
"next.js",
"oauth",
"kinto"
],
"type": "module",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "pnpm clean && tsc",
"clean": "rm -rf index.*",
"test": "vitest run -c ../utils/vitest.config.ts"
},
"files": [
"*.js",
"*.d.ts*",
"src"
],
"peerDependencies": {
"kinto": "^15.0.0"
},
"dependencies": {
"@auth/core": "workspace:*"
},
"devDependencies": {
"kinto": "^15.0.0"
}
}
239 changes: 239 additions & 0 deletions packages/adapter-kinto/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { KintoClient } from "kinto"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "@auth/core/adapters"

interface KintoAdapterOptions {
client: KintoClient
bucket?: string
}

export function toAdapterUser(record: any): AdapterUser {
return !record
? null
: {
id: record.id,
email: record.email,
emailVerified: record.emailVerified
? new Date(record.emailVerified)
: null,
name: record.name,
image: record.image,
}
}

export function toAdapterAccount(record: any): AdapterAccount {
return !record
? null
: {
id: record.id,
type: record.type,
userId: record.userId,
provider: record.provider,
providerAccountId: record.providerAccountId,
refresh_token: record.refresh_token,
access_token: record.access_token,
expires_at: record.expires_at,
token_type: record.token_type,
scope: record.scope,
id_token: record.id_token,
session_state: record.session_state,
}
}

export function toAdapterSession(record: any): AdapterSession {
return !record
? null
: {
id: record.id,
userId: record.userId,
sessionToken: record.sessionToken,
expires: new Date(record.expires),
}
}

export function toVerificationToken(record: any): VerificationToken {
return !record
? null
: {
identifier: record.identifier,
token: record.token,
expires: new Date(record.expires),
}
}

export function KintoAdapter({
client,
bucket = "auth",
collectionNames = {
users: "users",
accounts: "accounts",
sessions: "sessions",
verificationTokens: "verification-tokens",
},
}: KintoAdapterOptions): Adapter {
const collections = {
users: client.bucket(bucket).collection(collectionNames.users),
accounts: client.bucket(bucket).collection(collectionNames.accounts),
sessions: client.bucket(bucket).collection(collectionNames.sessions),
verificationTokens: client
.bucket(bucket)
.collection(collectionNames.verificationTokens),
}

async function ensureCollections() {
const existingCollections = new Set(
(await client.bucket(bucket).listCollections()).data.map((c) => c.id)
)
await Promise.all(
Object.keys(collections).map(async (key) => {
if (!existingCollections.has(key)) {
await client.bucket(bucket).createCollection(key)
}
})
)
}

return {
async createUser(data) {
await ensureCollections()
const result = await collections.users.createRecord(data)
return toAdapterUser(result.data)
},

async getUser(id) {
try {
await ensureCollections()
const result = await collections.users.getRecord(id)
return toAdapterUser(result.data)
} catch {
return null
}
},

async getUserByEmail(email) {
await ensureCollections()
const result = await collections.users.listRecords({ filters: { email } })
return result.data.length ? toAdapterUser(result.data[0]) : null
},

async getUserByAccount({ provider, providerAccountId }) {
await ensureCollections()
const result = await collections.accounts.listRecords({
filters: { provider, providerAccountId },
})
if (result.data.length) {
const user = await collections.users.getRecord(result.data[0].userId)
return toAdapterUser(user.data)
}
return null
},

async updateUser(data) {
await ensureCollections()
const result = await collections.users.updateRecord(data, { patch: true })
return toAdapterUser(result.data)
},

async deleteUser(id) {
await ensureCollections()
await collections.users.deleteRecord(id)
await Promise.all(
(
await collections.sessions.listRecords({ filters: { userId: id } })
).data.map(({ id }) => collections.sessions.deleteRecord(id))
)
await Promise.all(
(
await collections.accounts.listRecords({ filters: { userId: id } })
).data.map(({ id }) => collections.accounts.deleteRecord(id))
)
},

async linkAccount(data) {
await ensureCollections()
const result = await collections.accounts.createRecord(data)
return toAdapterAccount(result.data)
},

async unlinkAccount({ provider, providerAccountId }) {
await ensureCollections()
const result = await collections.accounts.listRecords({
filters: { provider, providerAccountId },
})
if (result.data.length) {
await collections.accounts.deleteRecord(result.data[0].id)
}
},

async createSession(data) {
await ensureCollections()
const result = await collections.sessions.createRecord(data)
return toAdapterSession(result.data)
},

async getSessionAndUser(sessionToken) {
await ensureCollections()
const sessionResult = await collections.sessions.listRecords({
filters: { sessionToken },
})
if (sessionResult.data.length) {
const session = sessionResult.data[0]
const user = await collections.users.getRecord(session.userId)
return {
session: toAdapterSession(session),
user: toAdapterUser(user.data),
}
}
return null
},

async updateSession(data) {
await ensureCollections()
const result = await collections.sessions.listRecords({
filters: { sessionToken: data.sessionToken },
})
if (result.data.length) {
const updated = await collections.sessions.updateRecord(
{ id: result.data[0].id, ...data },
{ patch: true }
)
return toAdapterSession(updated.data)
}
return null
},

async deleteSession(sessionToken) {
await ensureCollections()
const result = await collections.sessions.listRecords({
filters: { sessionToken },
})
if (result.data.length) {
await collections.sessions.deleteRecord(result.data[0].id)
}
},

async createVerificationToken(data) {
await ensureCollections()
const result = await collections.verificationTokens.createRecord(data)
return toVerificationToken(result.data)
},

async useVerificationToken({ identifier, token }) {
await ensureCollections()
const result = await collections.verificationTokens.listRecords({
filters: { identifier, token },
})
if (result.data.length) {
const verificationToken = result.data[0]
await collections.verificationTokens.deleteRecord(verificationToken.id)
return toVerificationToken(verificationToken)
}
return null
},
}
}
88 changes: 88 additions & 0 deletions packages/adapter-kinto/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { runBasicTests } from "utils/adapter"
import { KintoClient } from "kinto"
import {
KintoAdapter,
toAdapterUser,
toAdapterAccount,
toAdapterSession,
toVerificationToken,
} from "../src" // Adjust to the actual path of your adapter
import {
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken,
} from "@auth/core/adapters"

const client = new KintoClient("http://localhost:8888/v1") // Replace with your Kinto server URL
const bucket = "auth-test"

const collections = {
sessions: client.bucket(bucket).collection("sessions"),
users: client.bucket(bucket).collection("users"),
accounts: client.bucket(bucket).collection("accounts"),
verificationTokens: client.bucket(bucket).collection("verification-tokens"),
}

// Clear all records from the collections
async function clearCollections() {
await Promise.all(
Object.values(collections).map(async (collection) => {
const records = await collection.listRecords()
await Promise.all(
records.data.map((record) => collection.deleteRecord(record.id))
)
})
)
}

// Test setup
runBasicTests({
adapter: KintoAdapter({ client, bucket }),
db: {
async connect() {
// Ensure collections exist
const existingCollections = new Set(
(await client.bucket(bucket).listCollections()).data.map((c) => c.id)
)
await Promise.all(
Object.keys(collections).map(async (key) => {
if (!existingCollections.has(key)) {
return client.bucket(bucket).createCollection(key)
} else {
return Promise.resolve()
}
})
)
},
async disconnect() {
await clearCollections()
},
async user(id) {
try {
const record = await collections.users.getRecord(id)
return toAdapterUser(record.data)
} catch {
return null
}
},
async account({ provider, providerAccountId }) {
const result = await collections.accounts.listRecords({
filters: { provider, providerAccountId },
})
return toAdapterAccount(result.data.length ? result.data[0] : null)
},
async session(sessionToken) {
const result = await collections.sessions.listRecords({
filters: { sessionToken },
})
return toAdapterSession(result.data.length ? result.data[0] : null)
},
async verificationToken({ identifier, token }) {
const result = await collections.verificationTokens.listRecords({
filters: { identifier, token },
})
return toVerificationToken(result.data.length ? result.data[0] : null)
},
},
})
9 changes: 9 additions & 0 deletions packages/adapter-kinto/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../utils/tsconfig.json",
"compilerOptions": {
"outDir": ".",
"rootDir": "src"
},
"exclude": ["*.js", "*.d.ts"],
"include": ["src/**/*"]
}
Loading
Loading