mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
2 Commits
@auth/soli
...
feat/hasur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be6a85d7b5 | ||
|
|
946717523b |
10
packages/adapter-hasura/README.md
Normal file
10
packages/adapter-hasura/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<p align="center">
|
||||||
|
<br/>
|
||||||
|
<a href="https://next-auth.js.org" target="_blank"><img height="64px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a> <img height="64px" src="https://hasura.io/brand-assets/hasura-logo-primary-dark.svg" />
|
||||||
|
<h3 align="center"><b>Hasura Adapter</b> - NextAuth.js</h3>
|
||||||
|
<p align="center">
|
||||||
|
Open Source. Full Stack. Own Your Data.
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
TODO
|
||||||
44
packages/adapter-hasura/package.json
Normal file
44
packages/adapter-hasura/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "@next-auth/hasura-adapter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Hasura 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": "Balázs Orbán <info@balazsorban.com>",
|
||||||
|
"contributors": [],
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"index.d.ts"
|
||||||
|
],
|
||||||
|
"license": "ISC",
|
||||||
|
"keywords": [
|
||||||
|
"next-auth",
|
||||||
|
"next.js",
|
||||||
|
"hasura",
|
||||||
|
"graphql"
|
||||||
|
],
|
||||||
|
"private": false,
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"build": "tsc",
|
||||||
|
"test": "./tests/test.sh"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next-auth": "^4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@next-auth/adapter-test": "workspace:*",
|
||||||
|
"@next-auth/tsconfig": "workspace:*",
|
||||||
|
"next-auth": "workspace:*"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "@next-auth/adapter-test/jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
packages/adapter-hasura/src/graphql/models.ts
Normal file
49
packages/adapter-hasura/src/graphql/models.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { Account } from "next-auth"
|
||||||
|
import type {
|
||||||
|
AdapterSession,
|
||||||
|
AdapterUser,
|
||||||
|
VerificationToken,
|
||||||
|
} from "next-auth/adapters"
|
||||||
|
|
||||||
|
export interface Model<T> {
|
||||||
|
name: string
|
||||||
|
fields: Array<keyof T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Models {
|
||||||
|
User: Model<AdapterUser>
|
||||||
|
Account: Model<Account>
|
||||||
|
Session: Model<AdapterSession>
|
||||||
|
VerificationToken: Model<VerificationToken>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const models: Models = {
|
||||||
|
User: {
|
||||||
|
name: "User",
|
||||||
|
fields: ["email", "id", "image", "name", "emailVerified"],
|
||||||
|
},
|
||||||
|
Account: {
|
||||||
|
name: "Account",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"type",
|
||||||
|
"provider",
|
||||||
|
"providerAccountId",
|
||||||
|
"expires_at",
|
||||||
|
"token_type",
|
||||||
|
"scope",
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
"id_token",
|
||||||
|
"session_state",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Session: {
|
||||||
|
name: "Session",
|
||||||
|
fields: ["expires", "id", "sessionToken"],
|
||||||
|
},
|
||||||
|
VerificationToken: {
|
||||||
|
name: "VerificationToken",
|
||||||
|
fields: ["identifier", "token", "expires"],
|
||||||
|
},
|
||||||
|
}
|
||||||
1
packages/adapter-hasura/src/graphql/schema.gql
Normal file
1
packages/adapter-hasura/src/graphql/schema.gql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# TODO
|
||||||
209
packages/adapter-hasura/src/index.ts
Normal file
209
packages/adapter-hasura/src/index.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||||
|
import { format } from "./utils"
|
||||||
|
import { Models, Model, models } from "./graphql/models"
|
||||||
|
|
||||||
|
import type { Adapter, AdapterUser } from "next-auth/adapters"
|
||||||
|
import type { Account } from "next-auth"
|
||||||
|
|
||||||
|
export type { Models, Model }
|
||||||
|
|
||||||
|
export interface HasuraClientOptions {
|
||||||
|
/** GraphQL endpoint */
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* `X-Hasura-Admin-Secret` header value
|
||||||
|
*
|
||||||
|
* [Securing the GraphQL endpoint](https://hasura.io/docs/latest/deployment/securing-graphql-endpoint/)
|
||||||
|
*/
|
||||||
|
adminSecret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HasuraAdapterOptions {
|
||||||
|
client: HasuraClientOptions
|
||||||
|
models?: Partial<Models>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HasuraAdapter(options: HasuraAdapterOptions): Adapter {
|
||||||
|
const c = client(options.client)
|
||||||
|
const m: Models = {
|
||||||
|
User: { ...models.User, ...options.models?.User },
|
||||||
|
Account: { ...models.Account, ...options.models?.Account },
|
||||||
|
Session: { ...models.Session, ...options.models?.Session },
|
||||||
|
VerificationToken: {
|
||||||
|
...models.VerificationToken,
|
||||||
|
...options.models?.VerificationToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
async createUser(data) {
|
||||||
|
const result = await c.run<AdapterUser[]>(
|
||||||
|
/* GraphQL */ `
|
||||||
|
mutation ($data: [${m.User.name}_insert_input!]!) {
|
||||||
|
insert_${m.User.name}(objects: $data) {
|
||||||
|
returning {
|
||||||
|
${m.User.fields.join(" ")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ data }
|
||||||
|
)
|
||||||
|
return format.from(result?.[0])
|
||||||
|
},
|
||||||
|
async getUser(id) {
|
||||||
|
const result = await c.run<AdapterUser>(
|
||||||
|
/* GraphQL */ `
|
||||||
|
query ($id: String!) {
|
||||||
|
${m.User.name}(where: { id: { _eq: $id } }) {
|
||||||
|
${m.User.fields.join(" ")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ id }
|
||||||
|
)
|
||||||
|
|
||||||
|
return format.from(result)
|
||||||
|
},
|
||||||
|
async getUserByEmail(email) {
|
||||||
|
const result = await c.run<AdapterUser[]>(
|
||||||
|
/* GraphQL */ `
|
||||||
|
query ($email: String!) {
|
||||||
|
${m.User.name}(where: { email: { _eq: $email } }) {
|
||||||
|
${m.User.fields.join(" ")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{ email }
|
||||||
|
)
|
||||||
|
return result?.[0] ?? null
|
||||||
|
},
|
||||||
|
async getUserByAccount(provider_providerAccountId) {
|
||||||
|
const result = await c.run<Array<AdapterUser & { Accounts: Account }>>(
|
||||||
|
/* GraphQL */ `
|
||||||
|
query getUserByAccount($providerAccountId: String!, $provider: String!) {
|
||||||
|
${m.User.name}(
|
||||||
|
where: {Accounts: {providerAccountId: {_eq: $providerAccountId}, provider: {_eq: $provider}}}
|
||||||
|
) {
|
||||||
|
${m.User.fields.join(" ")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
provider_providerAccountId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!result?.length) return null
|
||||||
|
const { Accounts: _, ...user } = result[0]
|
||||||
|
return user
|
||||||
|
},
|
||||||
|
async updateUser({ id, ...input }) {
|
||||||
|
throw new HasuraAdapterError("`updateUser` not implemented")
|
||||||
|
},
|
||||||
|
async deleteUser(id) {
|
||||||
|
throw new HasuraAdapterError("`deleteUser` not implemented")
|
||||||
|
},
|
||||||
|
async linkAccount(data) {
|
||||||
|
throw new HasuraAdapterError("`linkAccount` not implemented")
|
||||||
|
},
|
||||||
|
async unlinkAccount(provider_providerAccountId) {
|
||||||
|
throw new HasuraAdapterError("`unlinkAccount` not implemented")
|
||||||
|
},
|
||||||
|
async getSessionAndUser(sessionToken) {
|
||||||
|
throw new HasuraAdapterError("`getSessionAndUser` not implemented")
|
||||||
|
},
|
||||||
|
async createSession(data) {
|
||||||
|
throw new HasuraAdapterError("`createSession` not implemented")
|
||||||
|
},
|
||||||
|
async updateSession({ sessionToken, ...input }) {
|
||||||
|
throw new HasuraAdapterError("`updateSession` not implemented")
|
||||||
|
},
|
||||||
|
async deleteSession(sessionToken) {
|
||||||
|
throw new HasuraAdapterError("`deleteSession` not implemented")
|
||||||
|
},
|
||||||
|
async createVerificationToken(input) {
|
||||||
|
throw new HasuraAdapterError("`createVerificationToken` not implemented")
|
||||||
|
},
|
||||||
|
async useVerificationToken(params) {
|
||||||
|
throw new HasuraAdapterError("`useVerificationToken` not implemented")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasuraAdapterError extends Error {
|
||||||
|
name = "HasuraAdapterError"
|
||||||
|
query?: string
|
||||||
|
variables?: any
|
||||||
|
constructor(
|
||||||
|
e: Error | string | Array<Error | string>,
|
||||||
|
query?: string,
|
||||||
|
variables?: any
|
||||||
|
) {
|
||||||
|
// @ts-expect-error
|
||||||
|
super(Array.isArray(e) ? e.map((e) => e.message ?? e).join("\n") : e.m ?? e)
|
||||||
|
this.query = query
|
||||||
|
this.variables = variables
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
message: this.message,
|
||||||
|
query: this.query,
|
||||||
|
variables: this.variables,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
const items = [
|
||||||
|
this.message,
|
||||||
|
this.query,
|
||||||
|
JSON.stringify(this.variables, null, 2),
|
||||||
|
].filter(Boolean)
|
||||||
|
return `${this.name}: ${items.join("\n")}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function client(options: HasuraClientOptions) {
|
||||||
|
if (!globalThis.fetch) {
|
||||||
|
throw new HasuraAdapterError("Please provide a `fetch` implementation")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url, adminSecret } = options
|
||||||
|
|
||||||
|
if (!adminSecret) {
|
||||||
|
throw new HasuraAdapterError("Please provide an API key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new HasuraAdapterError("Please provide a GraphQL endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async run<T>(
|
||||||
|
query: string,
|
||||||
|
variables?: Record<string, any>
|
||||||
|
): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Hasura-Admin-Secret": adminSecret,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query, variables }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data = {}, errors } = await response.json()
|
||||||
|
if (errors?.length) {
|
||||||
|
throw new HasuraAdapterError(errors, query, variables)
|
||||||
|
}
|
||||||
|
return Object.values(data)[0] as any
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HasuraAdapterError) throw error
|
||||||
|
throw new HasuraAdapterError(error as Error, query, variables)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { format, models }
|
||||||
26
packages/adapter-hasura/src/utils.ts
Normal file
26
packages/adapter-hasura/src/utils.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// https://github.com/honeinc/is-iso-date/blob/master/index.js
|
||||||
|
const isoDateRE =
|
||||||
|
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
|
||||||
|
|
||||||
|
function isDate(value: any) {
|
||||||
|
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const format = {
|
||||||
|
from<T extends Record<string, any> | null>(
|
||||||
|
data?: T
|
||||||
|
): T extends null ? null : T {
|
||||||
|
if (!data) return null as any
|
||||||
|
const newObject: Record<string, unknown> = {}
|
||||||
|
for (const key in data) {
|
||||||
|
const value = data[key]
|
||||||
|
if (isDate(value)) {
|
||||||
|
newObject[key] = new Date(value)
|
||||||
|
} else {
|
||||||
|
newObject[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newObject as any
|
||||||
|
},
|
||||||
|
}
|
||||||
8
packages/adapter-hasura/tsconfig.eslint.json
Normal file
8
packages/adapter-hasura/tsconfig.eslint.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["tests", "src"],
|
||||||
|
"exclude": [
|
||||||
|
"./*.js",
|
||||||
|
"./*.d.ts",
|
||||||
|
]
|
||||||
|
}
|
||||||
15
packages/adapter-hasura/tsconfig.json
Normal file
15
packages/adapter-hasura/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"tests",
|
||||||
|
"dist",
|
||||||
|
"jest.config.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user