From 2023c8dcbf2efdec2584f42874188fa030d05ebb Mon Sep 17 00:00:00 2001 From: adriangalilea Date: Fri, 5 Jul 2024 17:53:04 +0200 Subject: [PATCH 1/6] feat: adapter-dgraph updated client, notably: HS512, better error handling. --- packages/adapter-dgraph/src/lib/client.ts | 90 ++++++++++++++--------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/packages/adapter-dgraph/src/lib/client.ts b/packages/adapter-dgraph/src/lib/client.ts index de0fd1b143..a0760e9d16 100644 --- a/packages/adapter-dgraph/src/lib/client.ts +++ b/packages/adapter-dgraph/src/lib/client.ts @@ -1,81 +1,99 @@ -import * as jwt from "jsonwebtoken" +import * as jwt from "jsonwebtoken"; export interface DgraphClientParams { - endpoint: string + endpoint: string; /** * `X-Auth-Token` header value * * [Dgraph Cloud Authentication](https://dgraph.io/docs/cloud/cloud-api/overview/#dgraph-cloud-authentication) */ - authToken: string - /** [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ - jwtSecret?: string + authToken: string; + /** + * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) + */ + jwtSecret?: string; /** - * @default "RS256" + * @default "HS512" + * + * Note: The default JWT algorithm is now HS512, since [Dgraph now supports HS512 algorithm](https://github.com/dgraph-io/dgraph/pull/8912) and it aligns with NextAuth.js defaults. + * HS256 and RS256 are still supported for backward compatibility. * * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ - jwtAlgorithm?: "HS256" | "RS256" + jwtAlgorithm?: "HS512" | "HS256" | "RS256"; /** * @default "Authorization" * * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ - authHeader?: string + authHeader?: string; } export class DgraphClientError extends Error { - name = "DgraphClientError" + name = "DgraphClientError"; + query: string; + variables: any; + originalErrors: any[]; + constructor(errors: any[], query: string, variables: any) { - super(errors.map((error) => error.message).join("\n")) - console.error({ query, variables }) + super(`GraphQL query failed with ${errors.length} errors.`); + this.originalErrors = errors; + this.query = query; + this.variables = variables; + Error.captureStackTrace(this, this.constructor); } } export function client(params: DgraphClientParams) { if (!params.authToken) { - throw new Error("Dgraph client error: Please provide an API key") + throw new Error("Dgraph client error: Please provide an API key"); } if (!params.endpoint) { - throw new Error( - "Dgraph client error: Please provide a valid GraphQL endpoint" - ) + throw new Error("Dgraph client error: Please provide a valid GraphQL endpoint"); } const { endpoint, authToken, jwtSecret, - jwtAlgorithm = "HS256", + jwtAlgorithm = "HS512", authHeader = "Authorization", - } = params + } = params; + const headers: HeadersInit = { "Content-Type": "application/json", "X-Auth-Token": authToken, - } + }; - if (authHeader && jwtSecret) { + if (jwtSecret) { headers[authHeader] = jwt.sign({ nextAuth: true }, jwtSecret, { algorithm: jwtAlgorithm, - }) + }); } return { - async run( - query: string, - variables?: Record - ): Promise { - const response = await fetch(endpoint, { - method: "POST", - headers, - body: JSON.stringify({ query, variables }), - }) + async run(query: string, variables?: Record): Promise { + try { + const response = await fetch(endpoint, { + method: "POST", + headers, + body: JSON.stringify({ query, variables }), + }); - const { data = {}, errors } = await response.json() - if (errors?.length) { - throw new DgraphClientError(errors, query, variables) - } - return Object.values(data)[0] as any + if (!response.ok) { + throw new Error(`HTTP error ${response.status}: ${response.statusText}`); + } + + const { data = {}, errors } = await response.json(); + if (errors?.length) { + throw new DgraphClientError(errors, query, variables); + } + + return Object.values(data)[0] as T | null; + } catch (error) { + console.error(`Error executing GraphQL query: ${error}`, { query, variables }); + throw error; + } }, - } -} +}; +} \ No newline at end of file From c8b8aef40d9a56dbd8d1f8888e6bea05b6c731df Mon Sep 17 00:00:00 2001 From: adriangalilea Date: Fri, 5 Jul 2024 17:54:22 +0200 Subject: [PATCH 2/6] feat: adapter-dgraph improved adapter: native id's and general rework. --- packages/adapter-dgraph/src/index.ts | 365 ++++++++++++++++----------- 1 file changed, 216 insertions(+), 149 deletions(-) diff --git a/packages/adapter-dgraph/src/index.ts b/packages/adapter-dgraph/src/index.ts index 677a16c527..383699165b 100644 --- a/packages/adapter-dgraph/src/index.ts +++ b/packages/adapter-dgraph/src/index.ts @@ -15,10 +15,10 @@ * @module @auth/dgraph-adapter */ import { client as dgraphClient } from "./lib/client.js" -import { isDate, type Adapter } from "@auth/core/adapters" import type { DgraphClientParams } from "./lib/client.js" import * as defaultFragments from "./lib/graphql/fragments.js" -import { +import type { + Adapter, AdapterAccount, AdapterSession, AdapterUser, @@ -49,11 +49,13 @@ export function DgraphAdapter( options?: DgraphAdapterOptions ): Adapter { const c = dgraphClient(client) - const fragments = { ...defaultFragments, ...options?.fragments } + return { - async createUser(input: AdapterUser) { - const result = await c.run<{ user: any[] }>( + async createUser(user: AdapterUser): Promise { + // Remove the id from the user object so that it can be generated by Dgraph + const { id, ...userWithoutId } = user + const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` mutation ($input: [AddUserInput!]!) { addUser(input: $input) { @@ -64,13 +66,18 @@ export function DgraphAdapter( } ${fragments.User} `, - { input } + { input: [userWithoutId] } ) - return format.from(result?.user[0]) + if (!result || !result.user) { + throw new Error("Failed to create user") + } + + return result.user[0] }, - async getUser(id: string) { - const result = await c.run( + + async getUser(id: string): Promise { + return await c.run( /* GraphQL */ ` query ($id: ID!) { getUser(id: $id) { @@ -81,13 +88,12 @@ export function DgraphAdapter( `, { id } ) - - return format.from(result) }, - async getUserByEmail(email: string) { - const [user] = await c.run( + + async getUserByEmail(email: string): Promise { + const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` - query ($email: String = "") { + query ($email: String!) { queryUser(filter: { email: { eq: $email } }) { ...UserFragment } @@ -96,40 +102,47 @@ export function DgraphAdapter( `, { email } ) - return format.from(user) + return result?.user?.[0] || null }, - async getUserByAccount(provider_providerAccountId: { + + async getUserByAccount({ + provider, + providerAccountId, + }: { provider: string providerAccountId: string - }) { - const [account] = await c.run( + }): Promise { + const result = await c.run<{ user: AdapterUser }[]>( /* GraphQL */ ` - query ($providerAccountId: String = "", $provider: String = "") { + query ($providerAccountId: String!, $provider: String!) { queryAccount( filter: { - and: { - providerAccountId: { eq: $providerAccountId } - provider: { eq: $provider } - } + and: [ + { providerAccountId: { eq: $providerAccountId } } + { provider: { eq: $provider } } + ] } ) { user { ...UserFragment } - id } } ${fragments.User} `, - provider_providerAccountId + { providerAccountId, provider } ) - return format.from(account?.user) + return result?.[0]?.user || null }, - async updateUser({ id, ...input }: { id: string }) { - const result = await c.run( + + async updateUser( + user: Partial & { id: string } + ): Promise { + const { id, ...update } = user + const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` - mutation ($id: [ID!] = "", $input: UserPatch) { - updateUser(input: { filter: { id: $id }, set: $input }) { + mutation ($id: ID!, $input: UserPatch!) { + updateUser(input: { filter: { id: [$id] }, set: $input }) { user { ...UserFragment } @@ -137,55 +150,92 @@ export function DgraphAdapter( } ${fragments.User} `, - { id, input } + { id, input: update } ) - return format.from(result.user[0]) + + if (!result?.user?.[0]) { + throw new Error("Failed to update user") + } + + return result.user[0] }, - async deleteUser(id: string) { - const result = await c.run( + + async deleteUser(userId: string): Promise { + const fetchResult = await c.run<{ + user: (AdapterUser & { + accounts: { id: string }[] + sessions: { sessionToken: string }[] + })[] + }>( /* GraphQL */ ` - mutation ($id: [ID!] = "") { - deleteUser(filter: { id: $id }) { - numUids - user { - accounts { - id - } - sessions { - id - } + query ($userId: ID!) { + getUser(id: $userId) { + ...UserFragment + accounts { + id + } + sessions { + sessionToken } } } + ${fragments.User} `, - { id } + { userId } ) - const deletedUser = format.from(result.user[0]) + const user = fetchResult?.user?.[0] + if (!user) { + return null // User not found + } - await c.run( + const deleteResult = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` - mutation ($accounts: [ID!], $sessions: [ID!]) { - deleteAccount(filter: { id: $accounts }) { - numUids - } - deleteSession(filter: { id: $sessions }) { - numUids + mutation ($userId: [ID!]!) { + deleteUser(filter: { id: $userId }) { + user { + ...UserFragment + } } } + ${fragments.User} `, - { - sessions: deletedUser.sessions.map((x: any) => x.id), - accounts: deletedUser.accounts.map((x: any) => x.id), - } + { userId: [userId] } ) + const deletedUser = deleteResult?.user?.[0] + if (!deletedUser) { + throw new Error("Failed to delete user") + } + + if (user.accounts.length > 0 || user.sessions.length > 0) { + await c.run<{ + deleteAccount: { numUids: number } + deleteSession: { numUids: number } + }>( + /* GraphQL */ ` + mutation ($accountIds: [ID!], $sessionTokens: [String!]) { + deleteAccount(filter: { id: $accountIds }) { + numUids + } + deleteSession(filter: { sessionToken: $sessionTokens }) { + numUids + } + } + `, + { + accountIds: user.accounts.map((x) => x.id), + sessionTokens: user.sessions.map((x) => x.sessionToken), + } + ) + } + return deletedUser }, - async linkAccount(data: AdapterAccount) { - const { userId, ...input } = data - await c.run( + async linkAccount(account: AdapterAccount): Promise { + const { id, userId, ...inputWithoutId } = account + const result = await c.run<{ account: AdapterAccount[] }>( /* GraphQL */ ` mutation ($input: [AddAccountInput!]!) { addAccount(input: $input) { @@ -196,64 +246,90 @@ export function DgraphAdapter( } ${fragments.Account} `, - { input: { ...input, user: { id: userId } } } + { input: [{ ...inputWithoutId, user: { id: userId } }] } ) - return data + + if (!result?.account?.[0]) { + throw new Error("Failed to link account") + } + + return result.account[0] }, - async unlinkAccount(provider_providerAccountId: { - provider: string - providerAccountId: string - }) { - await c.run( + + async unlinkAccount( + account: Pick + ): Promise { + const { provider, providerAccountId } = account + const result = await c.run<{ account: AdapterAccount[] }>( /* GraphQL */ ` - mutation ($providerAccountId: String = "", $provider: String = "") { + mutation ($providerAccountId: String!, $provider: String!) { deleteAccount( filter: { - and: { - providerAccountId: { eq: $providerAccountId } - provider: { eq: $provider } - } + and: [ + { providerAccountId: { eq: $providerAccountId } } + { provider: { eq: $provider } } + ] } ) { - numUids + account { + ...AccountFragment + } } } + ${fragments.Account} `, - provider_providerAccountId + { providerAccountId, provider } ) + + return result?.account?.[0] }, - async getSessionAndUser(sessionToken: string) { - const [sessionAndUser] = await c.run( + async getSessionAndUser( + sessionToken: string + ): Promise<{ session: AdapterSession; user: AdapterUser } | null> { + const result = await c.run<{ + getSessionAndUser: { + session: AdapterSession + user: AdapterUser + }[] + }>( /* GraphQL */ ` - query ($sessionToken: String = "") { - querySession(filter: { sessionToken: { eq: $sessionToken } }) { - ...SessionFragment + query GetSessionAndUser($sessionToken: String!) { + getSessionAndUser(filter: { sessionToken: { eq: $sessionToken } }) { + session { + ...SessionFragment + } user { ...UserFragment } } } - ${fragments.User} ${fragments.Session} + ${fragments.User} `, { sessionToken } ) - if (!sessionAndUser) return null - - const { user, ...session } = sessionAndUser - return { - user: format.from(user), - session: { ...format.from(session), userId: user.id }, + const sessionAndUser = result?.getSessionAndUser?.[0] + if (!sessionAndUser || !sessionAndUser.user || !sessionAndUser.session) { + return null } + + return sessionAndUser }, - async createSession(data: AdapterSession) { - const { userId, ...input } = data - await c.run( + async createSession({ + sessionToken, + userId, + expires, + }: AdapterSession): Promise { + if (userId === undefined) { + throw new Error("userId is undefined in createSession") + } + + const result = await c.run<{ session: AdapterSession[] }>( /* GraphQL */ ` - mutation ($input: [AddSessionInput!]!) { + mutation CreateSession($input: [AddSessionInput!]!) { addSession(input: $input) { session { ...SessionFragment @@ -262,73 +338,81 @@ export function DgraphAdapter( } ${fragments.Session} `, - { input: { ...input, user: { id: userId } } } + { input: [{ sessionToken, expires, user: { id: userId } }] } ) - return data as any + if (!result?.session?.[0]) { + throw new Error("Failed to create session") + } + + return result.session[0] }, - async updateSession({ sessionToken, ...input }: { sessionToken: string }) { - const result = await c.run( + + async deleteSession( + sessionToken: string + ): Promise { + const result = await c.run<{ session: AdapterSession[] }>( /* GraphQL */ ` - mutation ($input: SessionPatch = {}, $sessionToken: String) { - updateSession( - input: { - filter: { sessionToken: { eq: $sessionToken } } - set: $input - } - ) { + mutation DeleteSession($sessionToken: String!) { + deleteSession(filter: { sessionToken: { eq: $sessionToken } }) { session { ...SessionFragment - user { - id - } } } } ${fragments.Session} `, - { sessionToken, input } - ) - const session = format.from(result.session[0]) - - if (!session?.user?.id) return null - - return { ...session, userId: session.user.id } - }, - async deleteSession(sessionToken: string) { - await c.run( - /* GraphQL */ ` - mutation ($sessionToken: String = "") { - deleteSession(filter: { sessionToken: { eq: $sessionToken } }) { - numUids - } - } - `, { sessionToken } ) + + return result?.session?.[0] || null }, - async createVerificationToken(input: VerificationToken) { - const result = await c.run( + async createVerificationToken( + input: VerificationToken + ): Promise { + const result = await c.run<{ + verificationToken: VerificationToken[] + }>( /* GraphQL */ ` - mutation ($input: [AddVerificationTokenInput!]!) { + mutation CreateVerificationToken( + $input: [AddVerificationTokenInput!]! + ) { addVerificationToken(input: $input) { - numUids + verificationToken { + identifier + token + expires + } } } `, - { input } + { input: [input] } ) - return format.from(result) + + const createdToken = result?.verificationToken?.[0] + if (!createdToken) { + throw new Error("Failed to create verification token") + } + + return createdToken }, - async useVerificationToken(params: { identifier: string; token: string }) { - const result = await c.run( + async useVerificationToken(params: { + identifier: string + token: string + }): Promise { + const result = await c.run<{ + verificationToken: VerificationToken[] + }>( /* GraphQL */ ` - mutation ($token: String = "", $identifier: String = "") { + mutation UseVerificationToken($token: String!, $identifier: String!) { deleteVerificationToken( filter: { - and: { token: { eq: $token }, identifier: { eq: $identifier } } + and: [ + { token: { eq: $token } } + { identifier: { eq: $identifier } } + ] } ) { verificationToken { @@ -341,24 +425,7 @@ export function DgraphAdapter( params ) - return format.from(result.verificationToken[0]) + return result?.verificationToken?.[0] || null }, } } - -export const format = { - from(object?: Record): T | null { - const newObject: Record = {} - if (!object) return null - for (const key in object) { - const value = object[key] - if (isDate(value)) { - newObject[key] = new Date(value) - } else { - newObject[key] = value - } - } - - return newObject as T - }, -} From 5e86d5babcf56b9fa8390eef26258246592c2092 Mon Sep 17 00:00:00 2001 From: adriangalilea Date: Fri, 5 Jul 2024 18:13:06 +0200 Subject: [PATCH 3/6] chore: dgraph-adapter docs --- .../pages/getting-started/adapters/dgraph.mdx | 39 +++++-------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/docs/pages/getting-started/adapters/dgraph.mdx b/docs/pages/getting-started/adapters/dgraph.mdx index aee857758e..0433df41b9 100644 --- a/docs/pages/getting-started/adapters/dgraph.mdx +++ b/docs/pages/getting-started/adapters/dgraph.mdx @@ -92,6 +92,12 @@ app.use( +### Schema + + + Note that this adapter is designed so that it uses Dgraph internal ID's, if you are interested in using external id's you should modify your schema `id: ID` to `id: String @id` for instance and modify the adapter methods(createUser, linkAccount...). + + ### Unsecure Schema The quickest way to use Dgraph is by applying the unsecure schema to your [local](https://dgraph.io/docs/graphql/admin/#modifying-a-schema) Dgraph instance or if using Dgraph [cloud](https://dgraph.io/docs/cloud/cloud-quick-start/#the-schema) you can paste the schema in the codebox to update. @@ -235,7 +241,7 @@ type VerificationToken expires: DateTime } -# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"","Algo":"HS256"} +# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"","Algo":"HS512"} ``` ### Dgraph.Authorization @@ -244,7 +250,7 @@ In order to secure your graphql backend define the `Dgraph.Authorization` object bottom of your schema and provide `authHeader` and `jwtSecret` values to the DgraphClient. ```js -# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"YOUR CUSTOM NAMESPACE HERE","Algo":"HS256"} +# Dgraph.Authorization {"VerificationKey":"","Header":"","Namespace":"YOUR CUSTOM NAMESPACE HERE","Algo":"HS512"} ``` ### VerificationKey and jwtSecret @@ -276,31 +282,4 @@ type VerificationRequest ### JWT session and `@auth` directive -Dgraph only works with HS256 or RS256 algorithms. If you want to use session jwt to securely interact with your dgraph -database you must customize next-auth `encode` and `decode` functions, as the default algorithm is HS512. You can -further customize the jwt with roles if you want to implement [`RBAC logic`](https://dgraph.io/docs/graphql/authorization/directive/#role-based-access-control). - -```js filename="./auth.js" -import NextAuth from "next-auth" -import * as jwt from "jsonwebtoken" - -export const { handlers, auth, signIn, signOut } = NextAuth({ - session: { - strategy: "jwt", - }, - jwt: { - secret: process.env.SECRET, - encode: async ({ secret, token }) => { - return jwt.sign({ ...token, userId: token.id }, secret, { - algorithm: "HS256", - expiresIn: 30 * 24 * 60 * 60, // 30 days - }) - }, - decode: async ({ secret, token }) => { - return jwt.verify(token, secret, { algorithms: ["HS256"] }) - }, - }, -}) -``` - -Once your `Dgraph.Authorization` is defined in your schema and the JWT settings are set, this will allow you to define [`@auth rules`](https://dgraph.io/docs/graphql/authorization/authorization-overview/) for every part of your schema. +Once your `Dgraph.Authorization` is defined in your schema and the JWT settings are set, this will allow you to define [`@auth rules`](https://dgraph.io/docs/graphql/schema/directives/auth/) for every part of your schema. From dc4b32d55f55acfebd870ac552b3f8b1762d06c7 Mon Sep 17 00:00:00 2001 From: adriangalilea Date: Fri, 5 Jul 2024 18:25:55 +0200 Subject: [PATCH 4/6] feat: adapter-dgraph remove non needed promises --- packages/adapter-dgraph/src/index.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/adapter-dgraph/src/index.ts b/packages/adapter-dgraph/src/index.ts index 383699165b..63a9954406 100644 --- a/packages/adapter-dgraph/src/index.ts +++ b/packages/adapter-dgraph/src/index.ts @@ -52,7 +52,7 @@ export function DgraphAdapter( const fragments = { ...defaultFragments, ...options?.fragments } return { - async createUser(user: AdapterUser): Promise { + async createUser(user: AdapterUser) { // Remove the id from the user object so that it can be generated by Dgraph const { id, ...userWithoutId } = user const result = await c.run<{ user: AdapterUser[] }>( @@ -76,7 +76,7 @@ export function DgraphAdapter( return result.user[0] }, - async getUser(id: string): Promise { + async getUser(id: string) { return await c.run( /* GraphQL */ ` query ($id: ID!) { @@ -90,7 +90,7 @@ export function DgraphAdapter( ) }, - async getUserByEmail(email: string): Promise { + async getUserByEmail(email: string) { const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` query ($email: String!) { @@ -111,7 +111,7 @@ export function DgraphAdapter( }: { provider: string providerAccountId: string - }): Promise { + }) { const result = await c.run<{ user: AdapterUser }[]>( /* GraphQL */ ` query ($providerAccountId: String!, $provider: String!) { @@ -137,7 +137,7 @@ export function DgraphAdapter( async updateUser( user: Partial & { id: string } - ): Promise { + ) { const { id, ...update } = user const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` @@ -160,7 +160,7 @@ export function DgraphAdapter( return result.user[0] }, - async deleteUser(userId: string): Promise { + async deleteUser(userId: string) { const fetchResult = await c.run<{ user: (AdapterUser & { accounts: { id: string }[] @@ -233,7 +233,7 @@ export function DgraphAdapter( return deletedUser }, - async linkAccount(account: AdapterAccount): Promise { + async linkAccount(account: AdapterAccount) { const { id, userId, ...inputWithoutId } = account const result = await c.run<{ account: AdapterAccount[] }>( /* GraphQL */ ` @@ -258,7 +258,7 @@ export function DgraphAdapter( async unlinkAccount( account: Pick - ): Promise { + ) { const { provider, providerAccountId } = account const result = await c.run<{ account: AdapterAccount[] }>( /* GraphQL */ ` @@ -286,7 +286,7 @@ export function DgraphAdapter( async getSessionAndUser( sessionToken: string - ): Promise<{ session: AdapterSession; user: AdapterUser } | null> { + ) { const result = await c.run<{ getSessionAndUser: { session: AdapterSession @@ -322,7 +322,7 @@ export function DgraphAdapter( sessionToken, userId, expires, - }: AdapterSession): Promise { + }: AdapterSession) { if (userId === undefined) { throw new Error("userId is undefined in createSession") } @@ -350,7 +350,7 @@ export function DgraphAdapter( async deleteSession( sessionToken: string - ): Promise { + ) { const result = await c.run<{ session: AdapterSession[] }>( /* GraphQL */ ` mutation DeleteSession($sessionToken: String!) { @@ -370,7 +370,7 @@ export function DgraphAdapter( async createVerificationToken( input: VerificationToken - ): Promise { + ) { const result = await c.run<{ verificationToken: VerificationToken[] }>( @@ -401,7 +401,7 @@ export function DgraphAdapter( async useVerificationToken(params: { identifier: string token: string - }): Promise { + }) { const result = await c.run<{ verificationToken: VerificationToken[] }>( From 9f7674eac28c6958f42ff203cea45a5bf1b4c667 Mon Sep 17 00:00:00 2001 From: adriangalilea Date: Fri, 5 Jul 2024 21:33:00 +0200 Subject: [PATCH 5/6] chore: adapter-dgraph prettier format --- .../pages/getting-started/adapters/dgraph.mdx | 5 +- packages/adapter-dgraph/src/index.ts | 27 ++---- packages/adapter-dgraph/src/lib/client.ts | 96 ++++++++++--------- 3 files changed, 63 insertions(+), 65 deletions(-) diff --git a/docs/pages/getting-started/adapters/dgraph.mdx b/docs/pages/getting-started/adapters/dgraph.mdx index 0433df41b9..0d86f3dba6 100644 --- a/docs/pages/getting-started/adapters/dgraph.mdx +++ b/docs/pages/getting-started/adapters/dgraph.mdx @@ -95,7 +95,10 @@ app.use( ### Schema - Note that this adapter is designed so that it uses Dgraph internal ID's, if you are interested in using external id's you should modify your schema `id: ID` to `id: String @id` for instance and modify the adapter methods(createUser, linkAccount...). + Note that this adapter is designed so that it uses Dgraph internal ID's, if + you are interested in using external id's you should modify your schema `id: + ID` to `id: String @id` for instance and modify the adapter + methods(createUser, linkAccount...). ### Unsecure Schema diff --git a/packages/adapter-dgraph/src/index.ts b/packages/adapter-dgraph/src/index.ts index 63a9954406..deee52860a 100644 --- a/packages/adapter-dgraph/src/index.ts +++ b/packages/adapter-dgraph/src/index.ts @@ -135,9 +135,7 @@ export function DgraphAdapter( return result?.[0]?.user || null }, - async updateUser( - user: Partial & { id: string } - ) { + async updateUser(user: Partial & { id: string }) { const { id, ...update } = user const result = await c.run<{ user: AdapterUser[] }>( /* GraphQL */ ` @@ -284,9 +282,7 @@ export function DgraphAdapter( return result?.account?.[0] }, - async getSessionAndUser( - sessionToken: string - ) { + async getSessionAndUser(sessionToken: string) { const result = await c.run<{ getSessionAndUser: { session: AdapterSession @@ -318,11 +314,7 @@ export function DgraphAdapter( return sessionAndUser }, - async createSession({ - sessionToken, - userId, - expires, - }: AdapterSession) { + async createSession({ sessionToken, userId, expires }: AdapterSession) { if (userId === undefined) { throw new Error("userId is undefined in createSession") } @@ -348,9 +340,7 @@ export function DgraphAdapter( return result.session[0] }, - async deleteSession( - sessionToken: string - ) { + async deleteSession(sessionToken: string) { const result = await c.run<{ session: AdapterSession[] }>( /* GraphQL */ ` mutation DeleteSession($sessionToken: String!) { @@ -368,9 +358,7 @@ export function DgraphAdapter( return result?.session?.[0] || null }, - async createVerificationToken( - input: VerificationToken - ) { + async createVerificationToken(input: VerificationToken) { const result = await c.run<{ verificationToken: VerificationToken[] }>( @@ -398,10 +386,7 @@ export function DgraphAdapter( return createdToken }, - async useVerificationToken(params: { - identifier: string - token: string - }) { + async useVerificationToken(params: { identifier: string; token: string }) { const result = await c.run<{ verificationToken: VerificationToken[] }>( diff --git a/packages/adapter-dgraph/src/lib/client.ts b/packages/adapter-dgraph/src/lib/client.ts index a0760e9d16..a9c366d50b 100644 --- a/packages/adapter-dgraph/src/lib/client.ts +++ b/packages/adapter-dgraph/src/lib/client.ts @@ -1,55 +1,57 @@ -import * as jwt from "jsonwebtoken"; +import * as jwt from "jsonwebtoken" export interface DgraphClientParams { - endpoint: string; + endpoint: string /** * `X-Auth-Token` header value * * [Dgraph Cloud Authentication](https://dgraph.io/docs/cloud/cloud-api/overview/#dgraph-cloud-authentication) */ - authToken: string; - /** + authToken: string + /** * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ - jwtSecret?: string; + jwtSecret?: string /** * @default "HS512" - * + * * Note: The default JWT algorithm is now HS512, since [Dgraph now supports HS512 algorithm](https://github.com/dgraph-io/dgraph/pull/8912) and it aligns with NextAuth.js defaults. * HS256 and RS256 are still supported for backward compatibility. * * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ - jwtAlgorithm?: "HS512" | "HS256" | "RS256"; + jwtAlgorithm?: "HS512" | "HS256" | "RS256" /** * @default "Authorization" * * [Using JWT and authorization claims](https://dgraph.io/docs/graphql/authorization/authorization-overview#using-jwts-and-authorization-claims) */ - authHeader?: string; + authHeader?: string } export class DgraphClientError extends Error { - name = "DgraphClientError"; - query: string; - variables: any; - originalErrors: any[]; + name = "DgraphClientError" + query: string + variables: any + originalErrors: any[] constructor(errors: any[], query: string, variables: any) { - super(`GraphQL query failed with ${errors.length} errors.`); - this.originalErrors = errors; - this.query = query; - this.variables = variables; - Error.captureStackTrace(this, this.constructor); + super(`GraphQL query failed with ${errors.length} errors.`) + this.originalErrors = errors + this.query = query + this.variables = variables + Error.captureStackTrace(this, this.constructor) } } export function client(params: DgraphClientParams) { if (!params.authToken) { - throw new Error("Dgraph client error: Please provide an API key"); + throw new Error("Dgraph client error: Please provide an API key") } if (!params.endpoint) { - throw new Error("Dgraph client error: Please provide a valid GraphQL endpoint"); + throw new Error( + "Dgraph client error: Please provide a valid GraphQL endpoint" + ) } const { @@ -58,42 +60,50 @@ export function client(params: DgraphClientParams) { jwtSecret, jwtAlgorithm = "HS512", authHeader = "Authorization", - } = params; + } = params const headers: HeadersInit = { "Content-Type": "application/json", "X-Auth-Token": authToken, - }; + } if (jwtSecret) { headers[authHeader] = jwt.sign({ nextAuth: true }, jwtSecret, { algorithm: jwtAlgorithm, - }); + }) } return { - async run(query: string, variables?: Record): Promise { - try { - const response = await fetch(endpoint, { - method: "POST", - headers, - body: JSON.stringify({ query, variables }), - }); - - if (!response.ok) { - throw new Error(`HTTP error ${response.status}: ${response.statusText}`); - } + async run( + query: string, + variables?: Record + ): Promise { + try { + const response = await fetch(endpoint, { + method: "POST", + headers, + body: JSON.stringify({ query, variables }), + }) - const { data = {}, errors } = await response.json(); - if (errors?.length) { - throw new DgraphClientError(errors, query, variables); - } + if (!response.ok) { + throw new Error( + `HTTP error ${response.status}: ${response.statusText}` + ) + } - return Object.values(data)[0] as T | null; - } catch (error) { - console.error(`Error executing GraphQL query: ${error}`, { query, variables }); - throw error; + const { data = {}, errors } = await response.json() + if (errors?.length) { + throw new DgraphClientError(errors, query, variables) } + + return Object.values(data)[0] as T | null + } catch (error) { + console.error(`Error executing GraphQL query: ${error}`, { + query, + variables, + }) + throw error + } }, -}; -} \ No newline at end of file + } +} From 444d1d56e0dda544a4507d3deeb641ea57133b81 Mon Sep 17 00:00:00 2001 From: adriangalilea Date: Sun, 7 Jul 2024 12:05:37 +0200 Subject: [PATCH 6/6] feat: adapter-dgraph new tests --- packages/adapter-dgraph/test/hs512.key | 1 + packages/adapter-dgraph/test/index.test.ts | 17 +++- packages/adapter-dgraph/test/private.key | 51 ---------- packages/adapter-dgraph/test/public.key | 14 --- packages/adapter-dgraph/test/test.sh | 109 +++++++++++++++++---- 5 files changed, 105 insertions(+), 87 deletions(-) create mode 100644 packages/adapter-dgraph/test/hs512.key delete mode 100644 packages/adapter-dgraph/test/private.key delete mode 100644 packages/adapter-dgraph/test/public.key diff --git a/packages/adapter-dgraph/test/hs512.key b/packages/adapter-dgraph/test/hs512.key new file mode 100644 index 0000000000..b8cab49b38 --- /dev/null +++ b/packages/adapter-dgraph/test/hs512.key @@ -0,0 +1 @@ +GnaXu1rdxsquR+3y17VWU+/o4rs+URBZJqQwEizWLec= diff --git a/packages/adapter-dgraph/test/index.test.ts b/packages/adapter-dgraph/test/index.test.ts index e6047784d2..8f1978da79 100644 --- a/packages/adapter-dgraph/test/index.test.ts +++ b/packages/adapter-dgraph/test/index.test.ts @@ -7,13 +7,22 @@ import path from "path" import type { DgraphClientParams } from "../src" +let jwtSecret; +try { + jwtSecret = fs.readFileSync(path.join(process.cwd(), "/test/hs512.key"), { + encoding: "utf8", + }); + console.log("Loaded JWT secret from file", jwtSecret); +} catch (error) { + console.error("Failed to load JWT secret from file:", error); + process.exit(1); +} + const params: DgraphClientParams = { endpoint: "http://localhost:8080/graphql", authToken: "test", - jwtAlgorithm: "RS256", - jwtSecret: fs.readFileSync(path.join(process.cwd(), "/test/private.key"), { - encoding: "utf8", - }), + jwtAlgorithm: "HS512", + jwtSecret: jwtSecret, } /** TODO: Add test to `dgraphClient` */ diff --git a/packages/adapter-dgraph/test/private.key b/packages/adapter-dgraph/test/private.key deleted file mode 100644 index 285573fee3..0000000000 --- a/packages/adapter-dgraph/test/private.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/packages/adapter-dgraph/test/public.key b/packages/adapter-dgraph/test/public.key deleted file mode 100644 index 9c694ed1ba..0000000000 --- a/packages/adapter-dgraph/test/public.key +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxqyvd82VacXMBLUADZt+ -euSNUNJ276XgvH4HW4ms5iQZDgYIPKxyaZ+wk8EMYSB1dymJ3WQpm0JKHqgTW+z/ -edfYFQXkduHN/zoIpxMAMyZGsTBidGo0xJSHTCDCdYCCBlG9R1ljjhf0l9ChBP7W -7lSXaRU/XS/tMH1qYMpsUwDav4G/RDI3A4t29JRGqU4mnFa5o3XBCxU4ANCp1JaQ -evzAYox8EGPZ1YZGmhRgca51dBeed9QKqWjfXP4wboC1ppglm+kPgFUaCiXB8Kyf -IixhlvzZiO4RLvZw+cILt586vXGzNy49eVUTiIOoTZuG/79pCeBS8BCbB4l6y274 -y42hUN83gHxQ32Y++DI40jz5iGN85Dj6yDDjKwvwqVhCx/kVJFrmyrTJz/E0cp38 -FeIi7D6e0eXj7G97K+wkNdc4oTs1DsDPzhO/7wxQOZIjvNp+DJAfxin5MbM+UKoo -pvJj3sUMHVrTteWxZg94mmLjg2KnJYBuSn8kiFPYQ0F5MjE7df4tDDTGJ/VEFIG5 -EkQffaNYhW0Z5ORLvW1R1Yd1/ew3UWo+mZ7XAUGLF6clsWSQvzSrrNMYzCk5Fa0L -wvMtQdEVLL3q7/KsEHD7N78EVlmEDlOtC21UidUqXnawCE1QIjAHqFsNNPR2j0lg -OoEjrGdzrvUg6hNV9m6CbSECAwEAAQ== ------END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/adapter-dgraph/test/test.sh b/packages/adapter-dgraph/test/test.sh index 46f4b62054..60020ee21c 100755 --- a/packages/adapter-dgraph/test/test.sh +++ b/packages/adapter-dgraph/test/test.sh @@ -1,29 +1,102 @@ #!/usr/bin/env bash -CONTAINER_NAME=authjs-dgraph +# ============================================================================== +# Initial Configuration +# ============================================================================== +CONTAINER_NAME="authjs-dgraph" +SCHEMA_FILE="src/lib/graphql/schema.gql" +HS512_KEY=$(test/test.schema.gql -PUBLIC_KEY=$(sed 's/$/\\n/' test/public.key | tr -d '\n') -echo "# Dgraph.Authorization {\"VerificationKey\":\"$PUBLIC_KEY\",\"Namespace\":\"https://dgraph.io/jwt/claims\",\"Header\":\"Authorization\",\"Algo\":\"RS256\"}" >>test/test.schema.gql + echo "----------------------------------------" + for ((i=1; i<=max_attempts; i++)); do + if $command; then + echo "${success_message}" + # Interactive or non-interactive handling + # [[ -t 0 ]] && read -p "Pausing, press any key to continue..." + return 0 + else + echo "${fail_message} attempt $i" + fi + sleep ${sleep_duration} + done + echo "${fail_message}: Condition not met after ${max_attempts} attempts." + echo "----------------------------------------" + return 1 +} -curl -X POST localhost:8080/admin/schema --data-binary '@test/test.schema.gql' +# Checks if Dgraph server is up and accessible +function check_dgraph_ready { + curl -sSf "http://localhost:${PORT_8080}/health" >/dev/null 2>&1 +} -printf "\nWaiting 5s for schema to be uploaded..." && sleep 5 +# Function to upload schema and check success +function upload_schema_and_check { + local response=$(echo "${FINAL_SCHEMA}" | curl -s -w "%{http_code}" -o /tmp/dgraph_response.json -X POST "http://localhost:${PORT_8080}/admin/schema" --data-binary "@-") + local http_code=$(echo "${response}" | tail -n1) # Extract only the HTTP status code + [[ "$http_code" -eq 200 ]] +} -# Always stop container, but exit with 1 when tests are failing -if vitest run -c ../utils/vitest.config.ts; then - docker stop "${CONTAINER_NAME}" +# ============================================================================== +# Main Execution +# ============================================================================== + +# Check and remove any existing container +if docker ps -a | grep -q "${CONTAINER_NAME}"; then + echo "Stopping and removing existing container..." + docker stop "${CONTAINER_NAME}" + docker rm "${CONTAINER_NAME}" +fi + +# Start the Dgraph container +echo "----------------------------------------" +echo "Starting Dgraph container..." +docker run -d --rm -p "${PORT_8080}:${PORT_8080}" -p "${PORT_9080}:${PORT_9080}" --name "${CONTAINER_NAME}" "${CONTAINER_IMAGE}" + +# Wait for Dgraph to start +if ! wait_for_condition check_dgraph_ready "Dgraph is up!" "Dgraph not up..."; then + echo "Dgraph failed to start." + docker stop "${CONTAINER_NAME}" + exit 1 +fi + +# Prepare the Dgraph schema without the last line +SCHEMA=$(sed '$ d' "${SCHEMA_FILE}") + +# Append the Dgraph authorization header +FINAL_SCHEMA="${SCHEMA} +# Dgraph.Authorization {\"VerificationKey\":\"${HS512_KEY}\",\"Namespace\":\"https://dgraph.io/jwt/claims\",\"Header\":\"Authorization\",\"Algo\":\"HS512\"}" + +# Proceed with uploading the schema to Dgraph, include proper handling for multiline JSON +if echo "${FINAL_SCHEMA}" | curl -s -w "%{http_code}" -o /tmp/dgraph_response.json -X POST "http://localhost:${PORT_8080}/admin/schema" --data-binary "@-"; then + echo "Schema has been successfully uploaded." +else + echo "Failed to upload schema." + exit 1 +fi + +# Run tests +if vitest run -c "../utils/vitest.config.ts"; then + echo "Tests passed." else - docker stop "${CONTAINER_NAME}" && exit 1 + echo "Tests failed." fi -rm test/test.schema.gql +# Always stop container after tests +echo "----------------------------------------" +echo "Stopping Dgraph container..." +docker stop "${CONTAINER_NAME}" \ No newline at end of file