UNPKG

@next-auth/dgraph-adapter

Version:

Dgraph adapter for next-auth.

528 lines (527 loc) 17.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DgraphAdapter = exports.format = void 0; /** * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}> * <p style={{fontWeight: "normal"}}>Official <a href="https://dgraph.io/docs">Dgraph</a> adapter for Auth.js / NextAuth.js.</p> * <a href="https://dgraph.io/"> * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/dgraph.svg" width="100"/> * </a> * </div> * * ## Installation * * ```bash npm2yarn2pnpm * npm install next-auth @next-auth/dgraph-adapter * ``` * * @module @next-auth/dgraph-adapter */ const client_1 = require("./client"); const utils_1 = require("./utils"); Object.defineProperty(exports, "format", { enumerable: true, get: function () { return utils_1.format; } }); const defaultFragments = __importStar(require("./graphql/fragments")); /** * ## Setup * * Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object: * * ```ts title="pages/api/auth/[...nextauth].js" * import NextAuth from "next-auth" * import { DgraphAdapter } from "@next-auth/dgraph-adapter" * * export default NextAuth({ * providers: [], * adapter: DgraphAdapter({ * endpoint: process.env.DGRAPH_GRAPHQL_ENDPOINT, * authToken: process.env.DGRAPH_GRAPHQL_KEY, * // you can omit the following properties if you are running an unsecure schema * authHeader: process.env.AUTH_HEADER, // default: "Authorization", * jwtSecret: process.env.SECRET, * }), * }) * ``` * * ### 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. * * :::warning * This approach is not secure or for production use, and does not require a `jwtSecret`. * ::: * * > This schema is adapted for use in Dgraph and based upon our main [schema](https://authjs.dev/reference/adapters#models) * * #### Example * *```graphql * type Account { * id: ID * type: String * provider: String @search(by: [hash]) * providerAccountId: String @search(by: [hash]) * refreshToken: String * expires_at: Int64 * accessToken: String * token_type: String * refresh_token: String * access_token: String * scope: String * id_token: String * session_state: String * user: User @hasInverse(field: "accounts") * } * type Session { * id: ID * expires: DateTime * sessionToken: String @search(by: [hash]) * user: User @hasInverse(field: "sessions") * } * type User { * id: ID * name: String * email: String @search(by: [hash]) * emailVerified: DateTime * image: String * accounts: [Account] @hasInverse(field: "user") * sessions: [Session] @hasInverse(field: "user") * } * * type VerificationToken { * id: ID * identifier: String @search(by: [hash]) * token: String @search(by: [hash]) * expires: DateTime * } *``` * * ### Secure schema * * For production deployments you will want to restrict the access to the types used * by next-auth. The main form of access control used in Dgraph is via `@auth` directive alongside types in the schema. * #### Example * * ```graphql * type Account * @auth( * delete: { rule: "{$nextAuth: { eq: true } }" } * add: { rule: "{$nextAuth: { eq: true } }" } * query: { rule: "{$nextAuth: { eq: true } }" } * update: { rule: "{$nextAuth: { eq: true } }" } * ) { * id: ID * type: String * provider: String @search(by: [hash]) * providerAccountId: String @search(by: [hash]) * refreshToken: String * expires_at: Int64 * accessToken: String * token_type: String * refresh_token: String * access_token: String * scope: String * id_token: String * session_state: String * user: User @hasInverse(field: "accounts") * } * type Session * @auth( * delete: { rule: "{$nextAuth: { eq: true } }" } * add: { rule: "{$nextAuth: { eq: true } }" } * query: { rule: "{$nextAuth: { eq: true } }" } * update: { rule: "{$nextAuth: { eq: true } }" } * ) { * id: ID * expires: DateTime * sessionToken: String @search(by: [hash]) * user: User @hasInverse(field: "sessions") * } * type User * @auth( * query: { * or: [ * { * rule: """ * query ($userId: String!) {queryUser(filter: { id: { eq: $userId } } ) {id}} * """ * } * { rule: "{$nextAuth: { eq: true } }" } * ] * } * delete: { rule: "{$nextAuth: { eq: true } }" } * add: { rule: "{$nextAuth: { eq: true } }" } * update: { * or: [ * { * rule: """ * query ($userId: String!) {queryUser(filter: { id: { eq: $userId } } ) {id}} * """ * } * { rule: "{$nextAuth: { eq: true } }" } * ] * } * ) { * id: ID * name: String * email: String @search(by: [hash]) * emailVerified: DateTime * image: String * accounts: [Account] @hasInverse(field: "user") * sessions: [Session] @hasInverse(field: "user") * } * * type VerificationToken * @auth( * delete: { rule: "{$nextAuth: { eq: true } }" } * add: { rule: "{$nextAuth: { eq: true } }" } * query: { rule: "{$nextAuth: { eq: true } }" } * update: { rule: "{$nextAuth: { eq: true } }" } * ) { * id: ID * identifier: String @search(by: [hash]) * token: String @search(by: [hash]) * expires: DateTime * } * * # Dgraph.Authorization {"VerificationKey":"<YOUR JWT SECRET HERE>","Header":"<YOUR AUTH HEADER HERE>","Namespace":"<YOUR CUSTOM NAMESPACE HERE>","Algo":"HS256"} * ``` * * ### Dgraph.Authorization * * In order to secure your graphql backend define the `Dgraph.Authorization` object at the * bottom of your schema and provide `authHeader` and `jwtSecret` values to the DgraphClient. * * ```js * # Dgraph.Authorization {"VerificationKey":"<YOUR JWT SECRET HERE>","Header":"<YOUR AUTH HEADER HERE>","Namespace":"YOUR CUSTOM NAMESPACE HERE","Algo":"HS256"} * ``` * * ### VerificationKey and jwtSecret * * This is the key used to sign the JWT. Ex. `process.env.SECRET` or `process.env.APP_SECRET`. * * ### Header and authHeader * * The `Header` tells Dgraph where to lookup a JWT within the headers of the incoming requests made to the dgraph server. * You have to configure it at the bottom of your schema file. This header is the same as the `authHeader` property you * provide when you instantiate the `DgraphClient`. * * ### The nextAuth secret * * The `$nextAuth` secret is securely generated using the `jwtSecret` and injected by the DgraphAdapter in order to allow interacting with the JWT DgraphClient for anonymous user requests made within the system `ie. login, register`. This allows * secure interactions to be made with all the auth types required by next-auth. You have to specify it for each auth rule of * each type defined in your secure schema. * * ```js * type VerificationRequest * @auth( * delete: { rule: "{$nextAuth: { eq: true } }" }, * add: { rule: "{$nextAuth: { eq: true } }" }, * query: { rule: "{$nextAuth: { eq: true } }" }, * update: { rule: "{$nextAuth: { eq: true } }" } * ) { * ... * } * ``` * * ### 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 * import * as jwt from "jsonwebtoken" * export default 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. **/ function DgraphAdapter(client, options) { const c = (0, client_1.client)(client); const fragments = { ...defaultFragments, ...options === null || options === void 0 ? void 0 : options.fragments }; return { async createUser(input) { const result = await c.run( /* GraphQL */ ` mutation ($input: [AddUserInput!]!) { addUser(input: $input) { user { ...UserFragment } } } ${fragments.User} `, { input }); return utils_1.format.from(result === null || result === void 0 ? void 0 : result.user[0]); }, async getUser(id) { const result = await c.run( /* GraphQL */ ` query ($id: ID!) { getUser(id: $id) { ...UserFragment } } ${fragments.User} `, { id }); return utils_1.format.from(result); }, async getUserByEmail(email) { const [user] = await c.run( /* GraphQL */ ` query ($email: String = "") { queryUser(filter: { email: { eq: $email } }) { ...UserFragment } } ${fragments.User} `, { email }); return utils_1.format.from(user); }, async getUserByAccount(provider_providerAccountId) { const [account] = await c.run( /* GraphQL */ ` query ($providerAccountId: String = "", $provider: String = "") { queryAccount( filter: { and: { providerAccountId: { eq: $providerAccountId } provider: { eq: $provider } } } ) { user { ...UserFragment } id } } ${fragments.User} `, provider_providerAccountId); return utils_1.format.from(account === null || account === void 0 ? void 0 : account.user); }, async updateUser({ id, ...input }) { const result = await c.run( /* GraphQL */ ` mutation ($id: [ID!] = "", $input: UserPatch) { updateUser(input: { filter: { id: $id }, set: $input }) { user { ...UserFragment } } } ${fragments.User} `, { id, input }); return utils_1.format.from(result.user[0]); }, async deleteUser(id) { const result = await c.run( /* GraphQL */ ` mutation ($id: [ID!] = "") { deleteUser(filter: { id: $id }) { numUids user { accounts { id } sessions { id } } } } `, { id }); const deletedUser = utils_1.format.from(result.user[0]); await c.run( /* GraphQL */ ` mutation ($accounts: [ID!], $sessions: [ID!]) { deleteAccount(filter: { id: $accounts }) { numUids } deleteSession(filter: { id: $sessions }) { numUids } } `, { sessions: deletedUser.sessions.map((x) => x.id), accounts: deletedUser.accounts.map((x) => x.id), }); return deletedUser; }, async linkAccount(data) { const { userId, ...input } = data; await c.run( /* GraphQL */ ` mutation ($input: [AddAccountInput!]!) { addAccount(input: $input) { account { ...AccountFragment } } } ${fragments.Account} `, { input: { ...input, user: { id: userId } } }); return data; }, async unlinkAccount(provider_providerAccountId) { await c.run( /* GraphQL */ ` mutation ($providerAccountId: String = "", $provider: String = "") { deleteAccount( filter: { and: { providerAccountId: { eq: $providerAccountId } provider: { eq: $provider } } } ) { numUids } } `, provider_providerAccountId); }, async getSessionAndUser(sessionToken) { const [sessionAndUser] = await c.run( /* GraphQL */ ` query ($sessionToken: String = "") { querySession(filter: { sessionToken: { eq: $sessionToken } }) { ...SessionFragment user { ...UserFragment } } } ${fragments.User} ${fragments.Session} `, { sessionToken }); if (!sessionAndUser) return null; const { user, ...session } = sessionAndUser; return { user: utils_1.format.from(user), session: { ...utils_1.format.from(session), userId: user.id }, }; }, async createSession(data) { const { userId, ...input } = data; await c.run( /* GraphQL */ ` mutation ($input: [AddSessionInput!]!) { addSession(input: $input) { session { ...SessionFragment } } } ${fragments.Session} `, { input: { ...input, user: { id: userId } } }); return data; }, async updateSession({ sessionToken, ...input }) { var _a; const result = await c.run( /* GraphQL */ ` mutation ($input: SessionPatch = {}, $sessionToken: String) { updateSession( input: { filter: { sessionToken: { eq: $sessionToken } } set: $input } ) { session { ...SessionFragment user { id } } } } ${fragments.Session} `, { sessionToken, input }); const session = utils_1.format.from(result.session[0]); if (!((_a = session === null || session === void 0 ? void 0 : session.user) === null || _a === void 0 ? void 0 : _a.id)) return null; return { ...session, userId: session.user.id }; }, async deleteSession(sessionToken) { await c.run( /* GraphQL */ ` mutation ($sessionToken: String = "") { deleteSession(filter: { sessionToken: { eq: $sessionToken } }) { numUids } } `, { sessionToken }); }, async createVerificationToken(input) { const result = await c.run( /* GraphQL */ ` mutation ($input: [AddVerificationTokenInput!]!) { addVerificationToken(input: $input) { numUids } } `, { input }); return utils_1.format.from(result); }, async useVerificationToken(params) { const result = await c.run( /* GraphQL */ ` mutation ($token: String = "", $identifier: String = "") { deleteVerificationToken( filter: { and: { token: { eq: $token }, identifier: { eq: $identifier } } } ) { verificationToken { ...VerificationTokenFragment } } } ${fragments.VerificationToken} `, params); return utils_1.format.from(result.verificationToken[0]); }, }; } exports.DgraphAdapter = DgraphAdapter;