UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

358 lines (356 loc) 12.4 kB
import { DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from "../../../constants.js"; import { clearSystemCache, getCache } from "../../../cache.js"; import { getSecret } from "../../../utils/get-secret.js"; import { createDefaultAccountability } from "../../../permissions/utils/create-default-accountability.js"; import { verifyAccessJWT } from "../../../utils/jwt.js"; import { RevisionsService } from "../../revisions.js"; import { UsersService } from "../../users.js"; import { getIPFromReq } from "../../../utils/get-ip-from-req.js"; import { TFAService } from "../../tfa.js"; import { AuthenticationService } from "../../authentication.js"; import isDirectusJWT from "../../../utils/is-directus-jwt.js"; import { GraphQLBigInt } from "../types/bigint.js"; import { rateLimiter } from "../../../middleware/rate-limiter-registration.js"; import { UtilsService } from "../../utils.js"; import { GraphQLVoid } from "../types/void.js"; import "../index.js"; import { useEnv } from "@directus/env"; import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from "@directus/errors"; import { GraphQLBoolean, GraphQLEnumType, GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql"; import { SchemaComposer } from "graphql-compose"; //#region src/services/graphql/resolvers/system-global.ts const env = useEnv(); /** * Globally available mutations */ function globalResolvers(gql, schemaComposer) { const AuthTokens = schemaComposer.createObjectTC({ name: "auth_tokens", fields: { access_token: GraphQLString, expires: GraphQLBigInt, refresh_token: GraphQLString } }); const AuthMode = new GraphQLEnumType({ name: "auth_mode", values: { json: { value: "json" }, cookie: { value: "cookie" }, session: { value: "session" } } }); schemaComposer.Mutation.addFields({ auth_login: { type: AuthTokens, args: { email: new GraphQLNonNull(GraphQLString), password: new GraphQLNonNull(GraphQLString), mode: AuthMode, otp: GraphQLString }, resolve: async (_, args, { req, res }) => { const accountability = createDefaultAccountability(); if (req?.ip) accountability.ip = req.ip; const userAgent = req?.get("user-agent"); if (userAgent) accountability.userAgent = userAgent; const origin = req?.get("origin"); if (origin) accountability.origin = origin; const authenticationService = new AuthenticationService({ accountability, schema: gql.schema }); const mode = args["mode"] ?? "json"; const { accessToken, refreshToken, expires } = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, { session: mode === "session", otp: args?.otp }); const payload = { expires }; if (mode === "json") { payload.refresh_token = refreshToken; payload.access_token = accessToken; } if (mode === "cookie") { res?.cookie(env["REFRESH_TOKEN_COOKIE_NAME"], refreshToken, REFRESH_COOKIE_OPTIONS); payload.access_token = accessToken; } if (mode === "session") res?.cookie(env["SESSION_COOKIE_NAME"], accessToken, SESSION_COOKIE_OPTIONS); return payload; } }, auth_refresh: { type: AuthTokens, args: { refresh_token: GraphQLString, mode: AuthMode }, resolve: async (_, args, { req, res }) => { const accountability = createDefaultAccountability(); if (req?.ip) accountability.ip = req.ip; const userAgent = req?.get("user-agent"); if (userAgent) accountability.userAgent = userAgent; const origin = req?.get("origin"); if (origin) accountability.origin = origin; const authenticationService = new AuthenticationService({ accountability, schema: gql.schema }); const mode = args["mode"] ?? "json"; let currentRefreshToken; if (mode === "json") currentRefreshToken = args["refresh_token"]; else if (mode === "cookie") currentRefreshToken = req?.cookies[env["REFRESH_TOKEN_COOKIE_NAME"]]; else if (mode === "session") { const token = req?.cookies[env["SESSION_COOKIE_NAME"]]; if (isDirectusJWT(token)) currentRefreshToken = verifyAccessJWT(token, getSecret()).session; } if (!currentRefreshToken) throw new InvalidPayloadError({ reason: `The refresh token is required in either the payload or cookie` }); const { accessToken, refreshToken, expires } = await authenticationService.refresh(currentRefreshToken, { session: mode === "session" }); const payload = { expires }; if (mode === "json") { payload.refresh_token = refreshToken; payload.access_token = accessToken; } if (mode === "cookie") { res?.cookie(env["REFRESH_TOKEN_COOKIE_NAME"], refreshToken, REFRESH_COOKIE_OPTIONS); payload.access_token = accessToken; } if (mode === "session") res?.cookie(env["SESSION_COOKIE_NAME"], accessToken, SESSION_COOKIE_OPTIONS); return payload; } }, auth_logout: { type: GraphQLBoolean, args: { refresh_token: GraphQLString, mode: AuthMode }, resolve: async (_, args, { req, res }) => { const accountability = createDefaultAccountability(); if (req?.ip) accountability.ip = req.ip; const userAgent = req?.get("user-agent"); if (userAgent) accountability.userAgent = userAgent; const origin = req?.get("origin"); if (origin) accountability.origin = origin; const authenticationService = new AuthenticationService({ accountability, schema: gql.schema }); const mode = args["mode"] ?? "json"; let currentRefreshToken; if (mode === "json") currentRefreshToken = args["refresh_token"]; else if (mode === "cookie") currentRefreshToken = req?.cookies[env["REFRESH_TOKEN_COOKIE_NAME"]]; else if (mode === "session") { const token = req?.cookies[env["SESSION_COOKIE_NAME"]]; if (isDirectusJWT(token)) currentRefreshToken = verifyAccessJWT(token, getSecret()).session; } if (!currentRefreshToken) throw new InvalidPayloadError({ reason: `The refresh token is required in either the payload or cookie` }); await authenticationService.logout(currentRefreshToken); if (req?.cookies[env["REFRESH_TOKEN_COOKIE_NAME"]]) res?.clearCookie(env["REFRESH_TOKEN_COOKIE_NAME"], REFRESH_COOKIE_OPTIONS); if (req?.cookies[env["SESSION_COOKIE_NAME"]]) res?.clearCookie(env["SESSION_COOKIE_NAME"], SESSION_COOKIE_OPTIONS); return true; } }, auth_password_request: { type: GraphQLBoolean, args: { email: new GraphQLNonNull(GraphQLString), reset_url: GraphQLString }, resolve: async (_, args, { req }) => { const accountability = createDefaultAccountability(); if (req?.ip) accountability.ip = req.ip; const userAgent = req?.get("user-agent"); if (userAgent) accountability.userAgent = userAgent; const origin = req?.get("origin"); if (origin) accountability.origin = origin; const service = new UsersService({ accountability, schema: gql.schema }); try { await service.requestPasswordReset(args["email"], args["reset_url"] || null); } catch (err) { if (isDirectusError(err, ErrorCode.InvalidPayload)) throw err; } return true; } }, auth_password_reset: { type: GraphQLBoolean, args: { token: new GraphQLNonNull(GraphQLString), password: new GraphQLNonNull(GraphQLString) }, resolve: async (_, args, { req }) => { const accountability = createDefaultAccountability(); if (req?.ip) accountability.ip = req.ip; const userAgent = req?.get("user-agent"); if (userAgent) accountability.userAgent = userAgent; const origin = req?.get("origin"); if (origin) accountability.origin = origin; await new UsersService({ accountability, schema: gql.schema }).resetPassword(args["token"], args["password"]); return true; } }, users_me_tfa_generate: { type: new GraphQLObjectType({ name: "users_me_tfa_generate_data", fields: { secret: { type: GraphQLString }, otpauth_url: { type: GraphQLString } } }), args: { password: new GraphQLNonNull(GraphQLString) }, resolve: async (_, args) => { if (!gql.accountability?.user) return null; const service = new TFAService({ accountability: gql.accountability, schema: gql.schema }); await new AuthenticationService({ accountability: gql.accountability, schema: gql.schema }).verifyPassword(gql.accountability.user, args["password"]); const { url, secret } = await service.generateTFA(gql.accountability.user); return { secret, otpauth_url: url }; } }, users_me_tfa_enable: { type: GraphQLBoolean, args: { otp: new GraphQLNonNull(GraphQLString), secret: new GraphQLNonNull(GraphQLString) }, resolve: async (_, args) => { if (!gql.accountability?.user) return null; await new TFAService({ accountability: gql.accountability, schema: gql.schema }).enableTFA(gql.accountability.user, args["otp"], args["secret"]); return true; } }, users_me_tfa_disable: { type: GraphQLBoolean, args: { otp: new GraphQLNonNull(GraphQLString) }, resolve: async (_, args) => { if (!gql.accountability?.user) return null; const service = new TFAService({ accountability: gql.accountability, schema: gql.schema }); if (await service.verifyOTP(gql.accountability.user, args["otp"]) === false) throw new InvalidPayloadError({ reason: `"otp" is invalid` }); await service.disableTFA(gql.accountability.user); return true; } }, utils_random_string: { type: GraphQLString, args: { length: GraphQLInt }, resolve: async (_, args) => { const { nanoid } = await import("nanoid"); if (args["length"] !== void 0 && (args["length"] < 1 || args["length"] > 500)) throw new InvalidPayloadError({ reason: `"length" must be between 1 and 500` }); return nanoid(args["length"] ? args["length"] : 32); } }, utils_sort: { type: GraphQLBoolean, args: { collection: new GraphQLNonNull(GraphQLString), item: new GraphQLNonNull(GraphQLID), to: new GraphQLNonNull(GraphQLID) }, resolve: async (_, args) => { const service = new UtilsService({ accountability: gql.accountability, schema: gql.schema }); const { item, to } = args; await service.sort(args["collection"], { item, to }); return true; } }, utils_revert: { type: GraphQLBoolean, args: { revision: new GraphQLNonNull(GraphQLID) }, resolve: async (_, args) => { await new RevisionsService({ accountability: gql.accountability, schema: gql.schema }).revert(args["revision"]); return true; } }, utils_cache_clear: { type: GraphQLVoid, resolve: async () => { if (gql.accountability?.admin !== true) throw new ForbiddenError(); const { cache } = getCache(); await cache?.clear(); await clearSystemCache(); } }, users_invite_accept: { type: GraphQLBoolean, args: { token: new GraphQLNonNull(GraphQLString), password: new GraphQLNonNull(GraphQLString) }, resolve: async (_, args) => { await new UsersService({ accountability: gql.accountability, schema: gql.schema }).acceptInvite(args["token"], args["password"]); return true; } }, users_register: { type: GraphQLBoolean, args: { email: new GraphQLNonNull(GraphQLString), password: new GraphQLNonNull(GraphQLString), verification_url: GraphQLString, first_name: GraphQLString, last_name: GraphQLString }, resolve: async (_, args, { req }) => { const service = new UsersService({ accountability: null, schema: gql.schema }); const ip = req ? getIPFromReq(req) : null; if (ip) await rateLimiter.consume(ip); await service.registerUser({ email: args.email, password: args.password, verification_url: args.verification_url, first_name: args.first_name, last_name: args.last_name }); return true; } }, users_register_verify: { type: GraphQLBoolean, args: { token: new GraphQLNonNull(GraphQLString) }, resolve: async (_, args) => { await new UsersService({ accountability: null, schema: gql.schema }).verifyRegistration(args.token); return true; } } }); } //#endregion export { globalResolvers };