@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
358 lines (356 loc) • 12.4 kB
JavaScript
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 };