Skip to content

Commit 7968d9b

Browse files
committed
Add @auth/kinto-adapter
1 parent 9d704a0 commit 7968d9b

File tree

6 files changed

+491
-67
lines changed

6 files changed

+491
-67
lines changed

packages/adapter-kinto/package.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "@auth/kinto-adapter",
3+
"version": "2.7.4",
4+
"description": "Kinto adapter for next-auth.",
5+
"homepage": "https://authjs.dev",
6+
"repository": "https://github.com/nextauthjs/next-auth",
7+
"bugs": {
8+
"url": "https://github.com/nextauthjs/next-auth/issues"
9+
},
10+
"author": "Les De Ridder <[email protected]>",
11+
"license": "ISC",
12+
"keywords": [
13+
"next-auth",
14+
"next.js",
15+
"oauth",
16+
"kinto"
17+
],
18+
"type": "module",
19+
"exports": {
20+
".": {
21+
"types": "./index.d.ts",
22+
"import": "./index.js"
23+
}
24+
},
25+
"private": false,
26+
"publishConfig": {
27+
"access": "public"
28+
},
29+
"scripts": {
30+
"build": "pnpm clean && tsc",
31+
"clean": "rm -rf index.*",
32+
"test": "vitest run -c ../utils/vitest.config.ts"
33+
},
34+
"files": [
35+
"*.js",
36+
"*.d.ts*",
37+
"src"
38+
],
39+
"peerDependencies": {
40+
"kinto": "^15.0.0"
41+
},
42+
"dependencies": {
43+
"@auth/core": "workspace:*"
44+
},
45+
"devDependencies": {
46+
"kinto": "^15.0.0"
47+
}
48+
}

packages/adapter-kinto/src/index.ts

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { KintoClient } from "kinto"
2+
import type {
3+
Adapter,
4+
AdapterUser,
5+
AdapterAccount,
6+
AdapterSession,
7+
VerificationToken,
8+
} from "@auth/core/adapters"
9+
10+
interface KintoAdapterOptions {
11+
client: KintoClient
12+
bucket?: string
13+
}
14+
15+
export function toAdapterUser(record: any): AdapterUser {
16+
return !record
17+
? null
18+
: {
19+
id: record.id,
20+
email: record.email,
21+
emailVerified: record.emailVerified
22+
? new Date(record.emailVerified)
23+
: null,
24+
name: record.name,
25+
image: record.image,
26+
}
27+
}
28+
29+
export function toAdapterAccount(record: any): AdapterAccount {
30+
return !record
31+
? null
32+
: {
33+
id: record.id,
34+
type: record.type,
35+
userId: record.userId,
36+
provider: record.provider,
37+
providerAccountId: record.providerAccountId,
38+
refresh_token: record.refresh_token,
39+
access_token: record.access_token,
40+
expires_at: record.expires_at,
41+
token_type: record.token_type,
42+
scope: record.scope,
43+
id_token: record.id_token,
44+
session_state: record.session_state,
45+
}
46+
}
47+
48+
export function toAdapterSession(record: any): AdapterSession {
49+
return !record
50+
? null
51+
: {
52+
id: record.id,
53+
userId: record.userId,
54+
sessionToken: record.sessionToken,
55+
expires: new Date(record.expires),
56+
}
57+
}
58+
59+
export function toVerificationToken(record: any): VerificationToken {
60+
return !record
61+
? null
62+
: {
63+
identifier: record.identifier,
64+
token: record.token,
65+
expires: new Date(record.expires),
66+
}
67+
}
68+
69+
export function KintoAdapter({
70+
client,
71+
bucket = "auth",
72+
collectionNames = {
73+
users: "users",
74+
accounts: "accounts",
75+
sessions: "sessions",
76+
verificationTokens: "verification-tokens",
77+
},
78+
}: KintoAdapterOptions): Adapter {
79+
const collections = {
80+
users: client.bucket(bucket).collection(collectionNames.users),
81+
accounts: client.bucket(bucket).collection(collectionNames.accounts),
82+
sessions: client.bucket(bucket).collection(collectionNames.sessions),
83+
verificationTokens: client
84+
.bucket(bucket)
85+
.collection(collectionNames.verificationTokens),
86+
}
87+
88+
async function ensureCollections() {
89+
const existingCollections = new Set(
90+
(await client.bucket(bucket).listCollections()).data.map((c) => c.id)
91+
)
92+
await Promise.all(
93+
Object.keys(collections).map(async (key) => {
94+
if (!existingCollections.has(key)) {
95+
await client.bucket(bucket).createCollection(key)
96+
}
97+
})
98+
)
99+
}
100+
101+
return {
102+
async createUser(data) {
103+
await ensureCollections()
104+
const result = await collections.users.createRecord(data)
105+
return toAdapterUser(result.data)
106+
},
107+
108+
async getUser(id) {
109+
try {
110+
await ensureCollections()
111+
const result = await collections.users.getRecord(id)
112+
return toAdapterUser(result.data)
113+
} catch {
114+
return null
115+
}
116+
},
117+
118+
async getUserByEmail(email) {
119+
await ensureCollections()
120+
const result = await collections.users.listRecords({ filters: { email } })
121+
return result.data.length ? toAdapterUser(result.data[0]) : null
122+
},
123+
124+
async getUserByAccount({ provider, providerAccountId }) {
125+
await ensureCollections()
126+
const result = await collections.accounts.listRecords({
127+
filters: { provider, providerAccountId },
128+
})
129+
if (result.data.length) {
130+
const user = await collections.users.getRecord(result.data[0].userId)
131+
return toAdapterUser(user.data)
132+
}
133+
return null
134+
},
135+
136+
async updateUser(data) {
137+
await ensureCollections()
138+
const result = await collections.users.updateRecord(data, { patch: true })
139+
return toAdapterUser(result.data)
140+
},
141+
142+
async deleteUser(id) {
143+
await ensureCollections()
144+
await collections.users.deleteRecord(id)
145+
await Promise.all(
146+
(
147+
await collections.sessions.listRecords({ filters: { userId: id } })
148+
).data.map(({ id }) => collections.sessions.deleteRecord(id))
149+
)
150+
await Promise.all(
151+
(
152+
await collections.accounts.listRecords({ filters: { userId: id } })
153+
).data.map(({ id }) => collections.accounts.deleteRecord(id))
154+
)
155+
},
156+
157+
async linkAccount(data) {
158+
await ensureCollections()
159+
const result = await collections.accounts.createRecord(data)
160+
return toAdapterAccount(result.data)
161+
},
162+
163+
async unlinkAccount({ provider, providerAccountId }) {
164+
await ensureCollections()
165+
const result = await collections.accounts.listRecords({
166+
filters: { provider, providerAccountId },
167+
})
168+
if (result.data.length) {
169+
await collections.accounts.deleteRecord(result.data[0].id)
170+
}
171+
},
172+
173+
async createSession(data) {
174+
await ensureCollections()
175+
const result = await collections.sessions.createRecord(data)
176+
return toAdapterSession(result.data)
177+
},
178+
179+
async getSessionAndUser(sessionToken) {
180+
await ensureCollections()
181+
const sessionResult = await collections.sessions.listRecords({
182+
filters: { sessionToken },
183+
})
184+
if (sessionResult.data.length) {
185+
const session = sessionResult.data[0]
186+
const user = await collections.users.getRecord(session.userId)
187+
return {
188+
session: toAdapterSession(session),
189+
user: toAdapterUser(user.data),
190+
}
191+
}
192+
return null
193+
},
194+
195+
async updateSession(data) {
196+
await ensureCollections()
197+
const result = await collections.sessions.listRecords({
198+
filters: { sessionToken: data.sessionToken },
199+
})
200+
if (result.data.length) {
201+
const updated = await collections.sessions.updateRecord(
202+
{ id: result.data[0].id, ...data },
203+
{ patch: true }
204+
)
205+
return toAdapterSession(updated.data)
206+
}
207+
return null
208+
},
209+
210+
async deleteSession(sessionToken) {
211+
await ensureCollections()
212+
const result = await collections.sessions.listRecords({
213+
filters: { sessionToken },
214+
})
215+
if (result.data.length) {
216+
await collections.sessions.deleteRecord(result.data[0].id)
217+
}
218+
},
219+
220+
async createVerificationToken(data) {
221+
await ensureCollections()
222+
const result = await collections.verificationTokens.createRecord(data)
223+
return toVerificationToken(result.data)
224+
},
225+
226+
async useVerificationToken({ identifier, token }) {
227+
await ensureCollections()
228+
const result = await collections.verificationTokens.listRecords({
229+
filters: { identifier, token },
230+
})
231+
if (result.data.length) {
232+
const verificationToken = result.data[0]
233+
await collections.verificationTokens.deleteRecord(verificationToken.id)
234+
return toVerificationToken(verificationToken)
235+
}
236+
return null
237+
},
238+
}
239+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { runBasicTests } from "utils/adapter"
2+
import { KintoClient } from "kinto"
3+
import {
4+
KintoAdapter,
5+
toAdapterUser,
6+
toAdapterAccount,
7+
toAdapterSession,
8+
toVerificationToken,
9+
} from "../src" // Adjust to the actual path of your adapter
10+
import {
11+
AdapterAccount,
12+
AdapterSession,
13+
AdapterUser,
14+
VerificationToken,
15+
} from "@auth/core/adapters"
16+
17+
const client = new KintoClient("http://localhost:8888/v1") // Replace with your Kinto server URL
18+
const bucket = "auth-test"
19+
20+
const collections = {
21+
sessions: client.bucket(bucket).collection("sessions"),
22+
users: client.bucket(bucket).collection("users"),
23+
accounts: client.bucket(bucket).collection("accounts"),
24+
verificationTokens: client.bucket(bucket).collection("verification-tokens"),
25+
}
26+
27+
// Clear all records from the collections
28+
async function clearCollections() {
29+
await Promise.all(
30+
Object.values(collections).map(async (collection) => {
31+
const records = await collection.listRecords()
32+
await Promise.all(
33+
records.data.map((record) => collection.deleteRecord(record.id))
34+
)
35+
})
36+
)
37+
}
38+
39+
// Test setup
40+
runBasicTests({
41+
adapter: KintoAdapter({ client, bucket }),
42+
db: {
43+
async connect() {
44+
// Ensure collections exist
45+
const existingCollections = new Set(
46+
(await client.bucket(bucket).listCollections()).data.map((c) => c.id)
47+
)
48+
await Promise.all(
49+
Object.keys(collections).map(async (key) => {
50+
if (!existingCollections.has(key)) {
51+
return client.bucket(bucket).createCollection(key)
52+
} else {
53+
return Promise.resolve()
54+
}
55+
})
56+
)
57+
},
58+
async disconnect() {
59+
await clearCollections()
60+
},
61+
async user(id) {
62+
try {
63+
const record = await collections.users.getRecord(id)
64+
return toAdapterUser(record.data)
65+
} catch {
66+
return null
67+
}
68+
},
69+
async account({ provider, providerAccountId }) {
70+
const result = await collections.accounts.listRecords({
71+
filters: { provider, providerAccountId },
72+
})
73+
return toAdapterAccount(result.data.length ? result.data[0] : null)
74+
},
75+
async session(sessionToken) {
76+
const result = await collections.sessions.listRecords({
77+
filters: { sessionToken },
78+
})
79+
return toAdapterSession(result.data.length ? result.data[0] : null)
80+
},
81+
async verificationToken({ identifier, token }) {
82+
const result = await collections.verificationTokens.listRecords({
83+
filters: { identifier, token },
84+
})
85+
return toVerificationToken(result.data.length ? result.data[0] : null)
86+
},
87+
},
88+
})

packages/adapter-kinto/tsconfig.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../utils/tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": ".",
5+
"rootDir": "src"
6+
},
7+
"exclude": ["*.js", "*.d.ts"],
8+
"include": ["src/**/*"]
9+
}

0 commit comments

Comments
 (0)