@wepublish/api
Version:
API core for we.publish.
767 lines • 34.1 kB
JavaScript
"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