UNPKG

@wepublish/api

Version:
767 lines 34.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createFetcher = exports.tokenFromRequest = exports.contextFromRequest = void 0; const tslib_1 = require("tslib"); const wrap_1 = require("@graphql-tools/wrap"); const client_1 = require("@prisma/client"); const api_1 = require("../../authentication-api/src"); const api_2 = require("../../mail-api/src"); const api_3 = require("../../settings-api/src"); const api_4 = require("../../utils-api/src"); const abort_controller_1 = tslib_1.__importDefault(require("abort-controller")); const apollo_server_express_1 = require("apollo-server-express"); const crypto_1 = tslib_1.__importDefault(require("crypto")); const dataloader_1 = tslib_1.__importDefault(require("dataloader")); const graphql_1 = require("graphql"); const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken")); const node_cache_1 = tslib_1.__importDefault(require("node-cache")); const node_fetch_1 = tslib_1.__importDefault(require("node-fetch")); const openid_client_1 = require("openid-client"); const article_1 = require("./db/article"); const common_1 = require("./db/common"); const page_1 = require("./db/page"); const error_1 = require("./error"); const event_query_1 = require("./graphql/event/event.query"); const create_safe_host_url_1 = require("./graphql/peer/create-safe-host-url"); const poll_public_queries_1 = require("./graphql/poll/poll.public-queries"); const memberContext_1 = require("./memberContext"); const api_5 = require("../../block-content-api/src"); /** * Peered article cache configuration and setup */ const ONE_HOUR_IN_SEC = 60 * 60; const ONE_MIN_IN_SEC = 60; const fetcherCache = new node_cache_1.default({ stdTTL: ONE_HOUR_IN_SEC, checkperiod: ONE_MIN_IN_SEC, deleteOnExpire: true, useClones: true }); function contextFromRequest(req, { hostURL, websiteURL, prisma, mediaAdapter, urlAdapter, oauth2Providers, hooks, mailProvider, mailContextOptions, paymentProviders, challenge, sessionTTL, hashCostFactor }) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const authService = new api_1.AuthenticationService(prisma); const token = tokenFromRequest(req); const session = token ? yield authService.getSessionByToken(token) : null; const isSessionValid = authService.isSessionValid(session); const peerDataLoader = new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.peer.findMany({ where: { id: { in: ids } } }), 'id'); })); const loaders = { navigationByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.navigation.findMany({ where: { id: { in: ids } }, include: { links: true } }), 'id'); })), navigationByKey: new dataloader_1.default((keys) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(keys, yield prisma.navigation.findMany({ where: { key: { in: keys } }, include: { links: true } }), 'key'); })), authorsByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.author.findMany({ where: { id: { in: ids } }, include: { links: true } }), 'id'); })), authorsBySlug: new dataloader_1.default((slugs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(slugs, yield prisma.author.findMany({ where: { slug: { in: slugs } }, include: { links: true } }), 'slug'); })), images: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.image.findMany({ where: { id: { in: ids } }, include: { focalPoint: true } }), 'id'); })), articles: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.article.findMany({ where: { id: { in: ids } }, include: { draft: { include: { properties: true, authors: true, socialMediaAuthors: true } }, pending: { include: { properties: true, authors: true, socialMediaAuthors: true } }, published: { include: { properties: true, authors: true, socialMediaAuthors: true } } } }), 'id'); })), publicArticles: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, (yield prisma.article.findMany({ where: { id: { in: ids }, OR: [ { publishedId: { not: null } }, { pendingId: { not: null } } ] }, include: { published: { include: { properties: true, authors: true, socialMediaAuthors: true } }, pending: { include: { properties: true, authors: true, socialMediaAuthors: true } } } })).map(article_1.articleWithRevisionsToPublicArticle), 'id'); })), pages: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.page.findMany({ where: { id: { in: ids } }, include: { draft: { include: { properties: true } }, pending: { include: { properties: true } }, published: { include: { properties: true } } } }), 'id'); })), publicPagesByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, (yield prisma.page.findMany({ where: { id: { in: ids }, OR: [ { published: { isNot: null } }, { pending: { isNot: null } } ] }, include: { published: { include: { properties: true } }, pending: { include: { properties: true } } } })).map(page_1.pageWithRevisionsToPublicPage), 'id'); })), publicPagesBySlug: new dataloader_1.default((slugs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(slugs, (yield prisma.page.findMany({ where: { OR: [ { published: { is: { slug: { in: slugs } } } }, { pending: { is: { slug: { in: slugs } } } } ] }, include: { published: { include: { properties: true } }, pending: { include: { properties: true } } } })).map(page_1.pageWithRevisionsToPublicPage), 'slug'); })), events: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.event.findMany({ where: { id: { in: ids } } }), 'id'); })), userRolesByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.userRole.findMany({ where: { id: { in: ids } } }), 'id'); })), mailLogsByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.mailLog.findMany({ where: { mailIdentifier: { in: ids } } }), 'id'); })), peer: peerDataLoader, peerBySlug: new dataloader_1.default((slugs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(slugs, yield prisma.peer.findMany({ where: { slug: { in: slugs } } }), 'slug'); })), peerSchema: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { const peers = yield peerDataLoader.loadMany(ids); return Promise.all(peers.map((peer) => tslib_1.__awaiter(this, void 0, void 0, function* () { var _a; try { if (!peer) return null; if (peer instanceof Error) { console.error(peer); return null; } const peerTimeout = ((_a = (yield prisma.setting.findUnique({ where: { name: api_3.SettingName.PEERING_TIMEOUT_MS } }))) === null || _a === void 0 ? void 0 : _a.value) || parseInt(process.env.PEERING_TIMEOUT_IN_MS) || 10 * 1000; // 10 Seconds timeout in ms const fetcher = createFetcher((0, create_safe_host_url_1.createSafeHostUrl)(peer.hostURL, 'v1'), peer.token, peerTimeout); return (0, wrap_1.wrapSchema)({ schema: yield (0, wrap_1.schemaFromExecutor)(fetcher), executor: fetcher }); } catch (err) { console.error(err); return null; } }))); })), peerAdminSchema: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { const peers = yield peerDataLoader.loadMany(ids); return Promise.all(peers.map((peer) => tslib_1.__awaiter(this, void 0, void 0, function* () { var _b; try { if (!peer) return null; if (peer instanceof Error) { console.error(peer); return null; } const peerTimeout = ((_b = (yield prisma.setting.findUnique({ where: { name: api_3.SettingName.PEERING_TIMEOUT_MS } }))) === null || _b === void 0 ? void 0 : _b.value) || parseInt(process.env.PEERING_TIMEOUT_IN_MS) || 10 * 1000; // 10 Seconds timeout in ms const fetcher = createFetcher((0, create_safe_host_url_1.createSafeHostUrl)(peer.hostURL, 'v1/admin'), peer.token, peerTimeout); return (0, wrap_1.wrapSchema)({ schema: yield (0, wrap_1.schemaFromExecutor)(fetcher), executor: fetcher }); } catch (err) { console.error(err); return null; } }))); })), memberPlansByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.memberPlan.findMany({ where: { id: { in: ids } }, include: { availablePaymentMethods: true } }), 'id'); })), memberPlansBySlug: new dataloader_1.default((slugs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(slugs, yield prisma.memberPlan.findMany({ where: { slug: { in: slugs } }, include: { availablePaymentMethods: true } }), 'slug'); })), activeMemberPlansByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.memberPlan.findMany({ where: { id: { in: ids }, active: true }, include: { availablePaymentMethods: true } }), 'id'); })), activeMemberPlansBySlug: new dataloader_1.default((slugs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(slugs, yield prisma.memberPlan.findMany({ where: { slug: { in: slugs }, active: true }, include: { availablePaymentMethods: true } }), 'slug'); })), paymentMethodsByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.paymentMethod.findMany({ where: { id: { in: ids } } }), 'id'); })), activePaymentMethodsByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.paymentMethod.findMany({ where: { id: { in: ids }, active: true } }), 'id'); })), activePaymentMethodsBySlug: new dataloader_1.default((slugs) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(slugs, yield prisma.paymentMethod.findMany({ where: { slug: { in: slugs }, active: true } }), 'slug'); })), invoicesByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.invoice.findMany({ where: { id: { in: ids } }, include: { items: true } }), 'id'); })), paymentsByID: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.payment.findMany({ where: { id: { in: ids } } }), 'id'); })), pollById: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return Promise.all(ids.map(id => (0, poll_public_queries_1.getPoll)(id, prisma.poll))); })), eventById: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return Promise.all(ids.map(id => (0, event_query_1.getEvent)(id, prisma.event))); })), commentsById: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.comment.findMany({ where: { id: { in: ids } }, include: { overriddenRatings: true, revisions: { orderBy: { createdAt: 'asc' } } } }), 'id'); })), commentRatingSystemAnswers: new dataloader_1.default(() => tslib_1.__awaiter(this, void 0, void 0, function* () { return [ yield prisma.commentRatingSystemAnswer.findMany() ]; })), subscriptionsById: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.subscription.findMany({ where: { id: { in: ids } }, include: { periods: true, properties: true, deactivation: true } }), 'id'); })), usersById: new dataloader_1.default((ids) => tslib_1.__awaiter(this, void 0, void 0, function* () { return (0, api_4.createOptionalsArray)(ids, yield prisma.user.findMany({ where: { id: { in: ids } }, include: { address: true } }), 'id'); })), blockStyleById: new api_5.BlockStylesDataloaderService(prisma) }; const mailContext = new api_2.MailContext({ prisma, mailProvider, defaultFromAddress: mailContextOptions.defaultFromAddress, defaultReplyToAddress: mailContextOptions.defaultReplyToAddress }); const generateJWTWrapper = ({ expiresInMinutes, audience, id }) => { if (!process.env.JWT_SECRET_KEY) throw new Error('No JWT_SECRET_KEY defined in environment.'); return (0, api_4.generateJWT)({ id, secret: process.env.JWT_SECRET_KEY, issuer: hostURL, audience: audience !== null && audience !== void 0 ? audience : websiteURL, expiresInMinutes }); }; const verifyJWT = (token) => { if (!process.env.JWT_SECRET_KEY) throw new Error('No JWT_SECRET_KEY defined in environment.'); const ver = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET_KEY); return typeof ver === 'object' && 'sub' in ver ? ver.sub : ''; }; const memberContext = new memberContext_1.MemberContext({ loaders, prisma, paymentProviders, mailContext, getLoginUrlForUser(user) { const jwt = generateJWTWrapper({ id: user.id, expiresInMinutes: 10080 // One week in minutes }); return urlAdapter.getLoginURL(jwt); } }); return { hostURL, websiteURL, session: isSessionValid ? session : null, loaders, prisma, memberContext, mailContext, mediaAdapter, urlAdapter, oauth2Providers, paymentProviders, hooks, sessionTTL: sessionTTL !== null && sessionTTL !== void 0 ? sessionTTL : common_1.DefaultSessionTTL, hashCostFactor: hashCostFactor !== null && hashCostFactor !== void 0 ? hashCostFactor : common_1.DefaultBcryptHashCostFactor, getOauth2Clients() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return yield Promise.all(oauth2Providers.map((provider) => tslib_1.__awaiter(this, void 0, void 0, function* () { const issuer = yield openid_client_1.Issuer.discover(provider.discoverUrl); return { name: provider.name, provider, client: new issuer.Client({ client_id: provider.clientId, client_secret: provider.clientKey, redirect_uris: provider.redirectUri, response_types: ['code'] }) }; }))); }); }, authenticateUser() { if (!session || session.type !== api_1.AuthSessionType.User) { throw new apollo_server_express_1.AuthenticationError('Invalid user session!'); } if (!isSessionValid) { throw new error_1.TokenExpiredError(); } return session; }, optionalAuthenticateUser() { if (!session || session.type !== api_1.AuthSessionType.User || !isSessionValid) { return null; } return session; }, authenticateToken() { if (!session || session.type !== api_1.AuthSessionType.Token) { throw new apollo_server_express_1.AuthenticationError('Invalid token session!'); } return session; }, authenticate() { if (!session) { throw new apollo_server_express_1.AuthenticationError('Invalid session!'); } if (!isSessionValid) { throw new error_1.TokenExpiredError(); } return session; }, generateJWT: generateJWTWrapper, verifyJWT, createPaymentWithProvider({ paymentMethodID, invoice, saveCustomer, failureURL, successURL, user }) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const paymentMethod = yield loaders.activePaymentMethodsByID.load(paymentMethodID); const paymentProvider = paymentProviders.find(pp => pp.id === (paymentMethod === null || paymentMethod === void 0 ? void 0 : paymentMethod.paymentProviderID)); if (!paymentProvider) { throw new Error('paymentProvider not found'); } const payment = yield prisma.payment.create({ data: { paymentMethodID, invoiceID: invoice.id, state: client_1.PaymentState.created } }); const customer = user ? yield prisma.paymentProviderCustomer.findFirst({ where: { userId: user.id, paymentProviderID: paymentMethod.paymentProviderID } }) : null; const intent = yield paymentProvider.createIntent({ paymentID: payment.id, invoice, saveCustomer, successURL, failureURL, customerID: customer === null || customer === void 0 ? void 0 : customer.customerID }); const updatedPayment = yield prisma.payment.update({ where: { id: payment.id }, data: { state: intent.state, intentID: `${intent.intentID}`, intentData: intent.intentData, intentSecret: intent.intentSecret, paymentData: intent.paymentData, paymentMethodID: payment.paymentMethodID, invoiceID: payment.invoiceID } }); if (!updatedPayment) throw new Error('Error during updating payment'); // TODO: this check needs to be removed // Mark invoice as paid if (intent.state === client_1.PaymentState.paid) { yield prisma.invoice.update({ where: { id: invoice.id }, data: { paidAt: new Date() } }); } return updatedPayment; }); }, challenge }; }); } exports.contextFromRequest = contextFromRequest; function tokenFromRequest(req) { if (req === null || req === void 0 ? void 0 : req.headers.authorization) { const [, token] = req.headers.authorization.match(/Bearer (.+?$)/i) || []; return token || null; } return null; } exports.tokenFromRequest = tokenFromRequest; /** * Function that generate the key for the cache * @param params */ function generateCacheKey(params) { return (crypto_1.default // Hash function doesn't have to be crypto safe, just fast! .createHash('md5') .update(`${JSON.stringify(params.hostURL)}${JSON.stringify(params.variables)}${JSON.stringify(params.query)}`) .digest('hex')); } /** * Function that refreshes and initializes entries in the cache * @param params */ function loadFreshData(params) { return tslib_1.__awaiter(this, void 0, void 0, function* () { try { const abortController = new abort_controller_1.default(); const peerTimeOut = params.timeout ? params.timeout : 10 * 1000; // 10 Seconds timeout in ms // Since we use auto refresh cache we can safely set the timeout to 3sec setTimeout(() => abortController.abort(), peerTimeOut); const fetchResult = yield (0, node_fetch_1.default)(params.hostURL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${params.token}` }, body: JSON.stringify({ query: params.query, variables: params.variables, operationName: params.operationName }), signal: abortController.signal }); const res = yield fetchResult.json(); if ((fetchResult === null || fetchResult === void 0 ? void 0 : fetchResult.status) !== 200) { return { errors: [new graphql_1.GraphQLError(`Peer responded with invalid status: ${fetchResult === null || fetchResult === void 0 ? void 0 : fetchResult.status}`)] }; } const cacheValue = { data: res, queryParams: params }; fetcherCache.set(params.cacheKey, cacheValue); return res; } catch (err) { let errorMessage = err; if (err.type === 'aborted') { errorMessage = new Error(`Connection to peer (${params.hostURL}) timed out.`); } (0, api_4.logger)('context').error(`${errorMessage}`); return { errors: [err] }; } }); } function createFetcher(hostURL, token, peerTimeOut) { const loadData = ({ query, variables, operationName }) => tslib_1.__awaiter(this, void 0, void 0, function* () { // Initialize and prepare caching const fetchParams = { hostURL, variables, query, operationName, token, cacheKey: '', timeout: peerTimeOut }; fetchParams.cacheKey = generateCacheKey(fetchParams); const cachedData = fetcherCache.get(fetchParams.cacheKey); if (cachedData) { // Serve cached entries direct return cachedData.data; } return yield loadFreshData(fetchParams); }); return ({ variables, operationName, document }) => tslib_1.__awaiter(this, void 0, void 0, function* () { const query = (0, graphql_1.print)(document); return loadData({ query, variables, operationName }); }); } exports.createFetcher = createFetcher; //# sourceMappingURL=context.js.map