mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
2 Commits
@auth/hasu
...
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