diff --git a/src/core/client/count/index.ts b/src/core/client/count/index.ts index ef7cefae0..6735efc6a 100644 --- a/src/core/client/count/index.ts +++ b/src/core/client/count/index.ts @@ -71,9 +71,9 @@ function detectAndInject(opts: DetectAndInjectArgs = {}) { const { url, id, notext } = queryMap[ref]; // Compile the arguments used to generate the - const args: Record = { - url, + const args: Record = { id, + url, notext: notext ? "true" : "false", ref, }; diff --git a/src/core/server/app/handlers/api/account/confirm.ts b/src/core/server/app/handlers/api/account/confirm.ts index 5d3853f2e..a4a6095d6 100644 --- a/src/core/server/app/handlers/api/account/confirm.ts +++ b/src/core/server/app/handlers/api/account/confirm.ts @@ -15,7 +15,7 @@ import { sendConfirmationEmail, verifyConfirmTokenString, } from "coral-server/services/users/auth/confirm"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types"; @@ -38,7 +38,7 @@ export const confirmRequestHandler = ({ mongo, mailerQueue, signingConfig, -}: ConfirmRequestOptions): RequestHandler => { +}: ConfirmRequestOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -59,9 +59,7 @@ export const confirmRequestHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, logger, now } = req.coral; // Grab the requesting user. const requestingUser = req.user; @@ -99,7 +97,7 @@ export const confirmRequestHandler = ({ await userIDLimiter.test(req, targetUserID); - const log = coral.logger.child( + const log = logger.child( { targetUserID, requestingUserID: requestingUser.id, @@ -122,7 +120,7 @@ export const confirmRequestHandler = ({ signingConfig, // TODO: (wyattjoh) evaluate the use of required here. targetUser as Required, - coral.now + now ); log.trace("sent confirm email with token"); @@ -144,7 +142,7 @@ export const confirmCheckHandler = ({ mongo, signingConfig, config, -}: ConfirmCheckOptions): RequestHandler => { +}: ConfirmCheckOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -165,9 +163,7 @@ export const confirmCheckHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // TODO: evaluate verifying if the Tenant allows verifications to short circuit. @@ -189,7 +185,7 @@ export const confirmCheckHandler = ({ tenant, signingConfig, tokenString, - coral.now + now ); return res.sendStatus(204); @@ -209,7 +205,7 @@ export const confirmHandler = ({ mongo, signingConfig, config, -}: ConfirmOptions): RequestHandler => { +}: ConfirmOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -230,9 +226,7 @@ export const confirmHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { now, tenant } = req.coral; // Grab the token from the request. const tokenString = extractTokenFromRequest(req, true); @@ -247,7 +241,7 @@ export const confirmHandler = ({ } // Execute the reset. - await confirmEmail(mongo, tenant, signingConfig, tokenString, coral.now); + await confirmEmail(mongo, tenant, signingConfig, tokenString, now); return res.sendStatus(204); } catch (err) { diff --git a/src/core/server/app/handlers/api/account/download.ts b/src/core/server/app/handlers/api/account/download.ts index 57e273966..22a857036 100644 --- a/src/core/server/app/handlers/api/account/download.ts +++ b/src/core/server/app/handlers/api/account/download.ts @@ -10,7 +10,7 @@ import { sendUserDownload, verifyDownloadTokenString, } from "coral-server/services/users/download"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; const USER_ID_LIMITER_TTL = "1d"; @@ -32,7 +32,7 @@ export const accountDownloadHandler = ({ redis, signingConfig, config, -}: AccountDownloadOptions): RequestHandler => { +}: AccountDownloadOptions): RequestHandler => { const userIDLimiter = new RequestLimiter({ redis, ttl: USER_ID_LIMITER_TTL, @@ -43,9 +43,7 @@ export const accountDownloadHandler = ({ return async (req, res, next) => { try { - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Get the fields from the body. Validate will throw an error if the body // does not conform to the specification. @@ -71,7 +69,7 @@ export const accountDownloadHandler = ({ tenant, signingConfig, token, - coral.now + now ); // Only load comments since this download token was issued. @@ -97,7 +95,7 @@ export const accountDownloadCheckHandler = ({ redis, signingConfig, config, -}: AccountDownloadCheckOptions): RequestHandler => { +}: AccountDownloadCheckOptions): RequestHandler => { const userIDLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -108,9 +106,7 @@ export const accountDownloadCheckHandler = ({ return async (req, res, next) => { try { - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; const tokenString = extractTokenFromRequest(req, true); if (!tokenString) { @@ -130,7 +126,7 @@ export const accountDownloadCheckHandler = ({ tenant, signingConfig, tokenString, - coral.now + now ); return res.sendStatus(204); diff --git a/src/core/server/app/handlers/api/account/invite.ts b/src/core/server/app/handlers/api/account/invite.ts index bf6aacd28..aef274acf 100644 --- a/src/core/server/app/handlers/api/account/invite.ts +++ b/src/core/server/app/handlers/api/account/invite.ts @@ -8,7 +8,7 @@ import { redeem, verifyInviteTokenString, } from "coral-server/services/users/auth/invite"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; export type InviteCheckOptions = Pick< AppOptions, @@ -20,7 +20,7 @@ export const inviteCheckHandler = ({ signingConfig, mongo, config, -}: InviteCheckOptions): RequestHandler => { +}: InviteCheckOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -41,9 +41,7 @@ export const inviteCheckHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Grab the token from the request. const tokenString = extractTokenFromRequest(req, true); @@ -62,7 +60,7 @@ export const inviteCheckHandler = ({ tenant, signingConfig, tokenString, - coral.now + now ); return res.sendStatus(204); @@ -92,7 +90,7 @@ export const inviteHandler = ({ mongo, signingConfig, config, -}: InviteOptions): RequestHandler => { +}: InviteOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -113,9 +111,7 @@ export const inviteHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Grab the token from the request. const tokenString = extractTokenFromRequest(req, true); @@ -143,7 +139,7 @@ export const inviteHandler = ({ signingConfig, tokenString, { username, password }, - coral.now + now ); return res.sendStatus(204); diff --git a/src/core/server/app/handlers/api/account/notifications.ts b/src/core/server/app/handlers/api/account/notifications.ts index 9848017c1..ff7daccab 100644 --- a/src/core/server/app/handlers/api/account/notifications.ts +++ b/src/core/server/app/handlers/api/account/notifications.ts @@ -3,7 +3,7 @@ import { RequestLimiter } from "coral-server/app/request/limiter"; import { updateUserNotificationSettings } from "coral-server/models/user"; import { decodeJWT, extractTokenFromRequest } from "coral-server/services/jwt"; import { verifyUnsubscribeTokenString } from "coral-server/services/notifications/categories/unsubscribe"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; export type UnsubscribeCheckOptions = Pick< AppOptions, @@ -15,7 +15,7 @@ export const unsubscribeCheckHandler = ({ mongo, signingConfig, config, -}: UnsubscribeCheckOptions): RequestHandler => { +}: UnsubscribeCheckOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -36,9 +36,7 @@ export const unsubscribeCheckHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // TODO: evaluate verifying if the Tenant allows verifications to short circuit. @@ -60,7 +58,7 @@ export const unsubscribeCheckHandler = ({ tenant, signingConfig, tokenString, - coral.now + now ); return res.sendStatus(204); @@ -80,7 +78,7 @@ export const unsubscribeHandler = ({ mongo, signingConfig, config, -}: UnsubscribeOptions): RequestHandler => { +}: UnsubscribeOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -101,9 +99,7 @@ export const unsubscribeHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Grab the token from the request. const tokenString = extractTokenFromRequest(req, true); @@ -123,7 +119,7 @@ export const unsubscribeHandler = ({ tenant, signingConfig, tokenString, - coral.now + now ); // Unsubscribe the user from all notification types. diff --git a/src/core/server/app/handlers/api/auth/local/forgot.ts b/src/core/server/app/handlers/api/auth/local/forgot.ts index 71d949385..d8ebd9e7a 100644 --- a/src/core/server/app/handlers/api/auth/local/forgot.ts +++ b/src/core/server/app/handlers/api/auth/local/forgot.ts @@ -13,7 +13,7 @@ import { verifyResetTokenString, } from "coral-server/services/users/auth"; import { validateEmail } from "coral-server/services/users/helpers"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; export interface ForgotBody { email: string; @@ -34,7 +34,7 @@ export const forgotHandler = ({ mongo, signingConfig, mailerQueue, -}: ForgotOptions): RequestHandler => { +}: ForgotOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -55,9 +55,7 @@ export const forgotHandler = ({ // Limit based on the IP address. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, logger, now } = req.coral; // Check to ensure that the local integration has been enabled. if (!hasEnabledAuthIntegration(tenant, "local")) { @@ -75,7 +73,7 @@ export const forgotHandler = ({ // Limit based on the email address. await emailLimiter.test(req, email); - const log = coral.logger.child( + const log = logger.child( { email, tenantID: tenant.id, @@ -102,7 +100,7 @@ export const forgotHandler = ({ config, signingConfig, user, - req.coral!.now + now ); // Add the email to the processing queue. @@ -150,7 +148,7 @@ export const forgotResetHandler = ({ mongo, signingConfig, config, -}: ForgotResetOptions): RequestHandler => { +}: ForgotResetOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -171,9 +169,7 @@ export const forgotResetHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Check to ensure that the local integration has been enabled. if (!hasEnabledAuthIntegration(tenant, "local")) { @@ -206,7 +202,7 @@ export const forgotResetHandler = ({ signingConfig, tokenString, password, - coral.now + now ); return res.sendStatus(204); @@ -226,7 +222,7 @@ export const forgotCheckHandler = ({ mongo, signingConfig, config, -}: ForgotCheckOptions): RequestHandler => { +}: ForgotCheckOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -247,9 +243,7 @@ export const forgotCheckHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Check to ensure that the local integration has been enabled. if (!hasEnabledAuthIntegration(tenant, "local")) { @@ -274,7 +268,7 @@ export const forgotCheckHandler = ({ tenant, signingConfig, tokenString, - coral.now + now ); return res.sendStatus(204); diff --git a/src/core/server/app/handlers/api/auth/local/index.ts b/src/core/server/app/handlers/api/auth/local/index.ts index 5f850a78a..a2d0aaf16 100644 --- a/src/core/server/app/handlers/api/auth/local/index.ts +++ b/src/core/server/app/handlers/api/auth/local/index.ts @@ -1,6 +1,6 @@ import { AppOptions } from "coral-server/app"; import { handleLogout } from "coral-server/app/middleware/passport"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; export * from "./forgot"; export * from "./signup"; @@ -10,7 +10,11 @@ export type LogoutOptions = Pick; export const logoutHandler = ({ redis, -}: LogoutOptions): RequestHandler => async (req, res, next) => { +}: LogoutOptions): RequestHandler => async ( + req, + res, + next +) => { try { // Get the user on the request. const user = req.user; diff --git a/src/core/server/app/handlers/api/auth/local/link.ts b/src/core/server/app/handlers/api/auth/local/link.ts index 5fc3bfeb1..eff00e1c3 100644 --- a/src/core/server/app/handlers/api/auth/local/link.ts +++ b/src/core/server/app/handlers/api/auth/local/link.ts @@ -6,7 +6,7 @@ import { RequestLimiter } from "coral-server/app/request/limiter"; import { linkUsersAvailable } from "coral-server/models/tenant"; import { signTokenString } from "coral-server/services/jwt"; import { link } from "coral-server/services/users"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; export interface LinkBody { email: string; @@ -28,7 +28,7 @@ export const linkHandler = ({ mongo, signingConfig, config, -}: LinkOptions): RequestHandler => { +}: LinkOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -42,9 +42,7 @@ export const linkHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Check to ensure that the local integration has been enabled. if (!linkUsersAvailable(tenant)) { @@ -62,13 +60,7 @@ export const linkHandler = ({ // Account linking is complete! Return the new access token for the // request. - const token = await signTokenString( - signingConfig, - user, - tenant, - {}, - coral.now - ); + const token = await signTokenString(signingConfig, user, tenant, {}, now); return res.json({ token }); } catch (err) { diff --git a/src/core/server/app/handlers/api/auth/local/signup.ts b/src/core/server/app/handlers/api/auth/local/signup.ts index 2b2b2c328..e8117253e 100644 --- a/src/core/server/app/handlers/api/auth/local/signup.ts +++ b/src/core/server/app/handlers/api/auth/local/signup.ts @@ -10,7 +10,7 @@ import { hasEnabledAuthIntegration } from "coral-server/models/tenant"; import { LocalProfile, User } from "coral-server/models/user"; import { create } from "coral-server/services/users"; import { sendConfirmationEmail } from "coral-server/services/users/auth"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types"; @@ -37,7 +37,7 @@ export const signupHandler = ({ mongo, signingConfig, mailerQueue, -}: SignupOptions): RequestHandler => { +}: SignupOptions): RequestHandler => { const ipLimiter = new RequestLimiter({ redis, ttl: "10m", @@ -51,9 +51,7 @@ export const signupHandler = ({ // Rate limit based on the IP address and user agent. await ipLimiter.test(req, req.ip); - // Tenant is guaranteed at this point. - const tenant = req.coral!.tenant!; - const now = req.coral!.now; + const { tenant, now } = req.coral; // Check to ensure that the local integration has been enabled. if (!hasEnabledAuthIntegration(tenant, "local")) { diff --git a/src/core/server/app/handlers/api/dashboard/index.ts b/src/core/server/app/handlers/api/dashboard/index.ts index d902e5b61..21d5308c6 100644 --- a/src/core/server/app/handlers/api/dashboard/index.ts +++ b/src/core/server/app/handlers/api/dashboard/index.ts @@ -19,13 +19,17 @@ import { retrieveDailyUserMetrics, retrieveTodayUserMetrics, } from "coral-server/models/user/metrics"; -import { Request, RequestHandler } from "coral-server/types/express"; +import { + Request, + RequestHandler, + TenantCoralRequest, +} from "coral-server/types/express"; -function getMetricsOptions(req: Request) { +function getMetricsOptions(req: Request) { // Get the current Tenant on the request. - const { id: tenantID } = req.coral!.tenant!; + const { id: tenantID } = req.coral.tenant; - const now = req.coral!.now; + const now = req.coral.now; // To set a fixed date for the date, uncomment the line below. // const now = DateTime.utc(2020, 5, 5, 12, 30).toJSDate(); @@ -46,7 +50,11 @@ function getMetricsOptions(req: Request) { export const todayMetricsHandler = ({ mongo, -}: AppOptions): RequestHandler => async (req, res, next) => { +}: AppOptions): RequestHandler => async ( + req, + res, + next +) => { try { const { tenantID, siteID, tz, now } = getMetricsOptions(req); if (!siteID) { @@ -71,7 +79,11 @@ export const todayMetricsHandler = ({ export const totalMetricsHandler = ({ mongo, -}: AppOptions): RequestHandler => async (req, res, next) => { +}: AppOptions): RequestHandler => async ( + req, + res, + next +) => { try { const { tenantID, siteID } = getMetricsOptions(req); if (!siteID) { @@ -105,7 +117,11 @@ export const totalMetricsHandler = ({ export const hourlyCommentsMetricsHandler = ({ mongo, -}: AppOptions): RequestHandler => async (req, res, next) => { +}: AppOptions): RequestHandler => async ( + req, + res, + next +) => { try { const { tenantID, siteID, tz, now } = getMetricsOptions(req); if (!siteID) { @@ -130,7 +146,11 @@ export const hourlyCommentsMetricsHandler = ({ export const dailyUsersMetricsHandler = ({ mongo, -}: AppOptions): RequestHandler => async (req, res, next) => { +}: AppOptions): RequestHandler => async ( + req, + res, + next +) => { try { const { tenantID, tz, now } = getMetricsOptions(req); @@ -146,7 +166,11 @@ export const dailyUsersMetricsHandler = ({ export const todayStoriesMetricsHandler = ({ mongo, -}: AppOptions): RequestHandler => async (req, res, next) => { +}: AppOptions): RequestHandler => async ( + req, + res, + next +) => { try { const { tenantID, siteID, tz, now } = getMetricsOptions(req); if (!siteID) { @@ -158,6 +182,7 @@ export const todayStoriesMetricsHandler = ({ tenantID, siteID, tz, + 20, now ); diff --git a/src/core/server/app/handlers/api/graphql.ts b/src/core/server/app/handlers/api/graphql.ts index ccebc6185..48b45e1b1 100644 --- a/src/core/server/app/handlers/api/graphql.ts +++ b/src/core/server/app/handlers/api/graphql.ts @@ -4,7 +4,11 @@ import { CLIENT_ID_HEADER } from "coral-common/constants"; import { AppOptions } from "coral-server/app"; import { graphqlMiddleware } from "coral-server/app/middleware/graphql"; import GraphContext, { GraphContextOptions } from "coral-server/graph/context"; -import { Request, RequestHandler } from "coral-server/types/express"; +import { + Request, + RequestHandler, + TenantCoralRequest, +} from "coral-server/types/express"; export type GraphMiddlewareOptions = Pick< AppOptions, @@ -30,27 +34,15 @@ export const graphQLHandler = ({ config, metrics, ...options -}: GraphMiddlewareOptions): RequestHandler => { +}: GraphMiddlewareOptions): RequestHandler => { // Generate the schema hash. const schemaHash = generateSchemaHash(schema); return graphqlMiddleware( config, - async (req: Request) => { - if (!req.coral) { - throw new Error("coral was not set"); - } - + async (req: Request) => { // Pull out some useful properties from Coral. - const { id, now, tenant, cache, logger, persisted } = req.coral; - - if (!cache) { - throw new Error("cache was not set"); - } - - if (!tenant) { - throw new Error("tenant was not set"); - } + const { id, now, tenant, logger, persisted } = req.coral; // Create some new options to store the tenant context details inside. const opts: GraphContextOptions = { diff --git a/src/core/server/app/handlers/api/install.ts b/src/core/server/app/handlers/api/install.ts index 080ca01dc..44529834b 100644 --- a/src/core/server/app/handlers/api/install.ts +++ b/src/core/server/app/handlers/api/install.ts @@ -25,7 +25,11 @@ import { isInstalled, } from "coral-server/services/tenant"; import { create, CreateUser } from "coral-server/services/users"; -import { Request, RequestHandler } from "coral-server/types/express"; +import { + CoralRequest, + Request, + RequestHandler, +} from "coral-server/types/express"; import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types"; @@ -37,7 +41,7 @@ export type TenantInstallCheckHandlerOptions = Pick< export const installCheckHandler = ({ config, redis, -}: TenantInstallCheckHandlerOptions): RequestHandler => { +}: TenantInstallCheckHandlerOptions): RequestHandler => { const { managementEnabled, signingConfig } = managementSigningConfig(config); const limiter = new RequestLimiter({ redis, @@ -52,21 +56,16 @@ export const installCheckHandler = ({ // Limit based on the IP address. await limiter.test(req, req.ip); - if (!req.coral) { - return next(new Error("coral was not set")); - } + // Pull the tenant out. + const { tenant, cache } = req.coral; - if (!req.coral.cache) { - return next(new Error("cache was not set")); - } - - if (req.coral.tenant) { - // There's already a Tenant on the request! No need to process further. + // If there's already a Tenant on the request! No need to process further. + if (tenant) { return next(new TenantInstalledAlreadyError()); } // Check to see if the server already has a tenant installed. - const alreadyInstalled = await isInstalled(req.coral.cache.tenant); + const alreadyInstalled = await isInstalled(cache.tenant); if (!alreadyInstalled) { // No tenants are installed at all, we can of course proceed with the // install now. @@ -274,7 +273,7 @@ async function checkForInstallationToken( const { token } = await verifyInstallationTokenString( signingConfig, accessToken, - req.coral!.now + req.coral.now ); // Check to see that the domain on the token matches the hostname on diff --git a/src/core/server/app/handlers/api/oembed/oembed.ts b/src/core/server/app/handlers/api/oembed/oembed.ts index 33f1934ae..e4fe56f7a 100644 --- a/src/core/server/app/handlers/api/oembed/oembed.ts +++ b/src/core/server/app/handlers/api/oembed/oembed.ts @@ -5,7 +5,7 @@ import { validate } from "coral-server/app/request/body"; import { supportsMediaType } from "coral-server/models/tenant"; import { translate } from "coral-server/services/i18n"; import { fetchOEmbedResponse } from "coral-server/services/oembed"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; const OEmbedQuerySchema = Joi.object().keys({ url: Joi.string().uri().required(), @@ -21,12 +21,12 @@ interface OEmbedQuery { export type OembedHandler = Pick; -export const oembedHandler = ({ i18n }: OembedHandler): RequestHandler => { +export const oembedHandler = ({ + i18n, +}: OembedHandler): RequestHandler => { // TODO: add some kind of rate limiting or spam protection return async (req, res, next) => { - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant } = req.coral; try { const { type, url, maxWidth }: OEmbedQuery = validate( diff --git a/src/core/server/app/handlers/api/remoteMedia/index.ts b/src/core/server/app/handlers/api/remoteMedia/index.ts index f01ab60be..77d5cf51f 100644 --- a/src/core/server/app/handlers/api/remoteMedia/index.ts +++ b/src/core/server/app/handlers/api/remoteMedia/index.ts @@ -1,22 +1,17 @@ import { searchGiphy } from "coral-server/services/giphy"; -import { Request, RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; -export const gifSearchHandler: RequestHandler = async ( - req: Request, +export const gifSearchHandler: RequestHandler = async ( + req, res, next ) => { - if (!req.coral) { - return next(new Error("coral was not set")); - } + const { tenant } = req.coral; if (!req.query.query) { return next(new Error("search query required")); } - const coral = req.coral; - const tenant = coral.tenant!; - try { const results = await searchGiphy( req.query.query, diff --git a/src/core/server/app/handlers/api/story/active.ts b/src/core/server/app/handlers/api/story/active.ts new file mode 100644 index 000000000..6471969bd --- /dev/null +++ b/src/core/server/app/handlers/api/story/active.ts @@ -0,0 +1,102 @@ +import Joi from "@hapi/joi"; +import { DateTime } from "luxon"; + +import { AppOptions } from "coral-server/app"; +import { validate } from "coral-server/app/request/body"; +import { calculateTotalPublishedCommentCount } from "coral-server/models/comment"; +import { retrieveTopStoryMetrics } from "coral-server/models/comment/metrics"; +import { retrieveSite } from "coral-server/models/site"; +import { retrieveManyStories, Story } from "coral-server/models/story"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; + +export type Options = Pick; + +const ActiveStoriesQuerySchema = Joi.object().keys({ + callback: Joi.string().allow("").optional(), + siteID: Joi.string().required(), +}); + +interface ActiveStoriesQuery { + callback: string; + siteID: string; +} + +/** + * ActiveHandlerResponse is part of the Active Stories API. Changes to this + * interface should be completed only after a deprecation cycle. + */ +interface ActiveHandlerResponse { + stories: Array<{ + id: string; + url: string; + title: string | null; + image: string | null; + publishedAt: Date | null; + count: number; + }>; +} + +export const activeHandler = ({ + mongo, +}: Options): RequestHandler => async (req, res, next) => { + try { + // Grab the Tenant. + const { tenant, now } = req.coral; + + // Ensure we have a siteID on the query. + const { siteID }: ActiveStoriesQuery = validate( + ActiveStoriesQuerySchema, + req.query + ); + + // Check to see that this site does exist for this Tenant. + const site = await retrieveSite(mongo, tenant.id, siteID); + if (!site) { + throw new Error("site not found"); + } + + // Find top active stories in the last 24 hours. + const start = DateTime.fromJSDate(now).minus({ hours: 24 }).toJSDate(); + const results = await retrieveTopStoryMetrics( + mongo, + tenant.id, + siteID, + 5, + start, + now + ); + + // Fetch all the stories for each count. This will be returned in the same + // ordering of the counts. + const stories = await retrieveManyStories( + mongo, + tenant.id, + results.map(({ _id }) => _id) + ); + + // Ensure that all entries are not null. + if (!stories.every((story) => story) || results.length !== stories.length) { + throw new Error("some stories with comments were not found"); + } + + // Generate the response using the existing order of the stories. + const response: ActiveHandlerResponse = { + // We verified above that there was no null stories in the array. + stories: (stories as Story[]).map( + ({ id, url, metadata, commentCounts }) => ({ + id, + url, + title: metadata?.title || null, + image: metadata?.image || null, + publishedAt: metadata?.publishedAt || null, + count: calculateTotalPublishedCommentCount(commentCounts.status), + }) + ), + }; + + // Respond using jsonp. + return res.jsonp(response); + } catch (err) { + return next(err); + } +}; diff --git a/src/core/server/app/handlers/api/story/count.ts b/src/core/server/app/handlers/api/story/count.ts index b38cb7bee..e217950c9 100644 --- a/src/core/server/app/handlers/api/story/count.ts +++ b/src/core/server/app/handlers/api/story/count.ts @@ -1,29 +1,58 @@ +import Joi from "@hapi/joi"; + import { AppOptions } from "coral-server/app"; +import { validate } from "coral-server/app/request/body"; import { calculateTotalPublishedCommentCount } from "coral-server/models/comment"; import { translate } from "coral-server/services/i18n"; import { find } from "coral-server/services/stories"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; const NUMBER_CLASS_NAME = "coral-count-number"; const TEXT_CLASS_NAME = "coral-count-text"; export type CountOptions = Pick; +const StoryCountQuerySchema = Joi.object().keys({ + // Required for JSONP support. + callback: Joi.string().allow("").optional(), + id: Joi.string().optional(), + url: Joi.string().optional(), + notext: Joi.string().allow("true", "false").required(), + ref: Joi.string().required(), +}); + +interface StoryCountQuery { + callback: string; + id?: string; + url?: string; + notext: "true" | "false"; + ref: string; +} + /** * countHandler returns translated comment counts using JSONP. */ export const countHandler = ({ mongo, i18n, -}: CountOptions): RequestHandler => async (req, res, next) => { +}: CountOptions): RequestHandler => async ( + req, + res, + next +) => { try { - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant } = req.coral; + // Ensure we have something to query with. + const { id, url, notext, ref }: StoryCountQuery = validate( + StoryCountQuerySchema, + req.query + ); + + // Try to query the story. const story = await find(mongo, tenant, { - id: req.query.id, - url: req.query.url, + id, + url, }); const count = story @@ -31,7 +60,7 @@ export const countHandler = ({ : 0; let html = ""; - if (req.query.notext === "true") { + if (notext === "true") { // We only need the count without the text. html = `${count}`; } else { @@ -52,7 +81,7 @@ export const countHandler = ({ // Respond using jsonp. res.jsonp({ // Reference from the client that we'll just send back as it is. - ref: req.query.ref, + ref, html, }); } catch (err) { diff --git a/src/core/server/app/handlers/api/story/index.ts b/src/core/server/app/handlers/api/story/index.ts index fb125334e..f1250b6fb 100644 --- a/src/core/server/app/handlers/api/story/index.ts +++ b/src/core/server/app/handlers/api/story/index.ts @@ -1 +1,2 @@ +export * from "./active"; export * from "./count"; diff --git a/src/core/server/app/handlers/api/user/download.ts b/src/core/server/app/handlers/api/user/download.ts index e74c5a88a..c0a68cee2 100644 --- a/src/core/server/app/handlers/api/user/download.ts +++ b/src/core/server/app/handlers/api/user/download.ts @@ -4,7 +4,7 @@ import { redeemDownloadToken, sendUserDownload, } from "coral-server/services/users/download"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; type AdminDownloadOptions = Pick< AppOptions, @@ -15,11 +15,9 @@ export const userDownloadHandler = ({ mongo, redis, signingConfig, -}: AdminDownloadOptions): RequestHandler => { +}: AdminDownloadOptions): RequestHandler => { return async (req, res, next) => { - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; const { token } = req.query; const { sub: userID } = decodeJWT(token); @@ -37,7 +35,7 @@ export const userDownloadHandler = ({ tenant, signingConfig, token, - coral.now + now ); // Only load comments since this download token was issued. diff --git a/src/core/server/app/middleware/csp/tenant.ts b/src/core/server/app/middleware/csp/tenant.ts index 4cebf1eea..220041992 100644 --- a/src/core/server/app/middleware/csp/tenant.ts +++ b/src/core/server/app/middleware/csp/tenant.ts @@ -20,16 +20,12 @@ async function retrieveSiteFromEmbed( mongo: Db, req: Request ): Promise { - if (!req.coral || !req.coral.tenant) { + const { tenant } = req.coral; + if (!tenant) { // There is no tenant for the request, don't add any headers. return null; } - // Pull the tenant and the logger from the request. - const { - coral: { tenant }, - } = req; - // Attempt to detect the site based on the query parameters. const { storyURL = "", diff --git a/src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts b/src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts index 9e3c4946c..1cef8e03f 100644 --- a/src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts +++ b/src/core/server/app/middleware/graphql/persistedQueryMiddleware.ts @@ -2,7 +2,7 @@ import { AppOptions } from "coral-server/app"; import { RawQueryNotAuthorized } from "coral-server/errors"; import { getPersistedQuery } from "coral-server/graph/persisted"; import { hasFeatureFlag } from "coral-server/models/tenant"; -import { RequestHandler } from "coral-server/types/express"; +import { RequestHandler, TenantCoralRequest } from "coral-server/types/express"; import { GQLFEATURE_FLAG, @@ -17,16 +17,10 @@ type PersistedQueryMiddlewareOptions = Pick< const persistedQueryMiddleware = ({ persistedQueriesRequired, persistedQueryCache, -}: PersistedQueryMiddlewareOptions): RequestHandler => async ( - req, - res, - next -) => { +}: PersistedQueryMiddlewareOptions): RequestHandler< + TenantCoralRequest +> => async (req, res, next) => { try { - if (!req.coral || !req.coral.tenant) { - throw new Error("tenant was not set"); - } - // Handle the payload if it is a persisted query. const body = req.method === "GET" ? req.query : req.body; const persisted = await getPersistedQuery(persistedQueryCache, body); diff --git a/src/core/server/app/middleware/installed.ts b/src/core/server/app/middleware/installed.ts index 3243a0937..8cbe86cf6 100644 --- a/src/core/server/app/middleware/installed.ts +++ b/src/core/server/app/middleware/installed.ts @@ -19,19 +19,11 @@ export const installedMiddleware = ({ res, next ) => { - if (!req.coral) { - return next(new Error("coral was not set")); - } - - if (!req.coral.cache) { - return next(new Error("cache was not set")); - } - const installed = await isInstalled(req.coral.cache.tenant, req.hostname); - // If Coral is installed, and redirectIfInstall is true, then it will redirect. - // If Coral is not installed, and redirectIfInstall is false, then it will also - // redirect. + // If Coral is installed, and redirectIfInstall is true, then it will + // redirect. If Coral is not installed, and redirectIfInstall is false, then + // it will also redirect. if (installed === redirectIfInstalled) { return res.redirect(redirectURL); } diff --git a/src/core/server/app/middleware/passport/index.ts b/src/core/server/app/middleware/passport/index.ts index f42917601..e8f5ca744 100644 --- a/src/core/server/app/middleware/passport/index.ts +++ b/src/core/server/app/middleware/passport/index.ts @@ -1,5 +1,5 @@ import Joi from "@hapi/joi"; -import { NextFunction, RequestHandler, Response } from "express"; +import { NextFunction, Response } from "express"; import { Redis } from "ioredis"; import jwt from "jsonwebtoken"; import passport, { Authenticator } from "passport"; @@ -20,7 +20,11 @@ import { revokeJWT, signTokenString, } from "coral-server/services/jwt"; -import { Request } from "coral-server/types/express"; +import { + Request, + RequestHandler, + TenantCoralRequest, +} from "coral-server/types/express"; export type VerifyCallback = ( err?: Error | null, @@ -67,7 +71,11 @@ const LogoutTokenSchema = Joi.object().keys({ exp: Joi.number().optional(), }); -export async function handleLogout(redis: Redis, req: Request, res: Response) { +export async function handleLogout( + redis: Redis, + req: Request, + res: Response +) { // Extract the token from the request. const token = extractTokenFromRequest(req); if (!token) { @@ -75,8 +83,7 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) { return res.sendStatus(204); } - // Coral is guaranteed at this point. - const { now } = req.coral!; + const { now } = req.coral; // Decode the token. const decoded = jwt.decode(token, {}); @@ -103,25 +110,15 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) { export async function handleSuccessfulLogin( user: User, signingConfig: JWTSigningConfig, - req: Request, + req: Request, res: Response, next: NextFunction ) { try { - // Coral is guaranteed at this point. - const coral = req.coral!; - - // Tenant is guaranteed at this point. - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Grab the token. - const token = await signTokenString( - signingConfig, - user, - tenant, - {}, - coral.now - ); + const token = await signTokenString(signingConfig, user, tenant, {}, now); // Set the cache control headers. res.header("Cache-Control", "private, no-cache, no-store, must-revalidate"); @@ -175,7 +172,7 @@ export async function handleOAuth2Callback( err: Error | null, user: User | null, signingConfig: JWTSigningConfig, - req: Request, + req: Request, res: Response ) { const path = "/embed/auth/callback"; @@ -189,9 +186,7 @@ export async function handleOAuth2Callback( } try { - // Tenant is guaranteed at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Grab the token. const accessToken = await signTokenString( @@ -199,7 +194,7 @@ export async function handleOAuth2Callback( user, tenant, {}, - coral.now + now ); // NOTE: disabled cookie support due to ITP/First Party Cookie bugs @@ -235,7 +230,7 @@ export const wrapOAuth2Authn = ( signingConfig: JWTSigningConfig, name: string, options?: any -): RequestHandler => (req: Request, res, next) => +): RequestHandler => (req, res, next) => authenticator.authenticate( name, { ...options, session: false }, @@ -262,7 +257,7 @@ export const wrapAuthn = ( signingConfig: JWTSigningConfig, name: string, options?: any -): RequestHandler => (req: Request, res, next) => +): RequestHandler => (req, res, next) => authenticator.authenticate( name, { ...options, session: false }, @@ -291,7 +286,7 @@ export const wrapAuthn = ( */ export const authenticate = ( authenticator: passport.Authenticator -): RequestHandler => (req, res, next) => +): RequestHandler => (req, res, next) => authenticator.authenticate( "jwt", { session: false }, diff --git a/src/core/server/app/middleware/passport/strategies/jwt.ts b/src/core/server/app/middleware/passport/strategies/jwt.ts index 50797f132..f41dc3de0 100644 --- a/src/core/server/app/middleware/passport/strategies/jwt.ts +++ b/src/core/server/app/middleware/passport/strategies/jwt.ts @@ -13,7 +13,7 @@ import { extractTokenFromRequest, StandardHeader, } from "coral-server/services/jwt"; -import { Request } from "coral-server/types/express"; +import { Request, TenantCoralRequest } from "coral-server/types/express"; import { JWTToken, JWTVerifier } from "./verifiers/jwt"; import { OIDCIDToken, OIDCVerifier } from "./verifiers/oidc"; @@ -123,7 +123,7 @@ export class JWTStrategy extends Strategy { this.verifiers = createVerifiers(options); } - public async authenticate(req: Request) { + public async authenticate(req: Request) { // Get the token from the request. const token = extractTokenFromRequest(req); if (!token) { @@ -132,7 +132,7 @@ export class JWTStrategy extends Strategy { return this.pass(); } - const { now, tenant } = req.coral!; + const { now, tenant } = req.coral; if (!tenant) { return this.error(new TenantNotFoundError(req.hostname)); } diff --git a/src/core/server/app/middleware/passport/strategies/local.ts b/src/core/server/app/middleware/passport/strategies/local.ts index f4ff172ea..80db4da53 100644 --- a/src/core/server/app/middleware/passport/strategies/local.ts +++ b/src/core/server/app/middleware/passport/strategies/local.ts @@ -11,14 +11,14 @@ import { retrieveUserWithProfile, verifyUserPassword, } from "coral-server/models/user"; -import { Request } from "coral-server/types/express"; +import { Request, TenantCoralRequest } from "coral-server/types/express"; const verifyFactory = ( mongo: Db, ipLimiter: RequestLimiter, emailLimiter: RequestLimiter ) => async ( - req: Request, + req: Request, emailInput: string, passwordInput: string, done: VerifyCallback @@ -34,8 +34,7 @@ const verifyFactory = ( await ipLimiter.test(req, req.ip); await emailLimiter.test(req, email); - // The tenant is guaranteed at this point. - const tenant = req.coral!.tenant!; + const { tenant } = req.coral; // Get the user from the database. const user = await retrieveUserWithProfile(mongo, tenant.id, { diff --git a/src/core/server/app/middleware/passport/strategies/oauth2.ts b/src/core/server/app/middleware/passport/strategies/oauth2.ts index e75552e0f..ec6f8a011 100644 --- a/src/core/server/app/middleware/passport/strategies/oauth2.ts +++ b/src/core/server/app/middleware/passport/strategies/oauth2.ts @@ -12,7 +12,7 @@ import { TenantCache, TenantCacheAdapter, } from "coral-server/services/tenant/cache"; -import { Request } from "coral-server/types/express"; +import { Request, TenantCoralRequest } from "coral-server/types/express"; interface OAuth2Integration { enabled: boolean; @@ -66,16 +66,14 @@ export default abstract class OAuth2Strategy< ): Promise; protected verifyCallback = async ( - req: Request, + req: Request, accessToken: string, refreshToken: string, profile: Profile, done: VerifyCallback ) => { try { - // Coral is defined at this point. - const coral = req.coral!; - const tenant = coral.tenant!; + const { tenant, now } = req.coral; // Get the integration. const integration = this.getIntegration(tenant.auth.integrations); @@ -85,7 +83,7 @@ export default abstract class OAuth2Strategy< tenant, integration as Required, profile, - coral.now + now ); if (!user) { return done(null); @@ -97,10 +95,9 @@ export default abstract class OAuth2Strategy< } }; - public authenticate(req: Request) { + public authenticate(req: Request) { try { - // Coral is defined at this point. - const tenant = req.coral!.tenant!; + const { tenant } = req.coral; // Get the integration. const integration = this.getIntegration(tenant.auth.integrations); diff --git a/src/core/server/app/middleware/passport/strategies/oidc/index.ts b/src/core/server/app/middleware/passport/strategies/oidc/index.ts index 51526a61b..09b3cff0b 100644 --- a/src/core/server/app/middleware/passport/strategies/oidc/index.ts +++ b/src/core/server/app/middleware/passport/strategies/oidc/index.ts @@ -29,7 +29,7 @@ import { } from "coral-server/services/tenant/cache"; import { findOrCreate } from "coral-server/services/users"; import { validateUsername } from "coral-server/services/users/helpers"; -import { Request } from "coral-server/types/express"; +import { Request, TenantCoralRequest } from "coral-server/types/express"; import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types"; @@ -355,9 +355,8 @@ export default class OIDCStrategy extends Strategy { return done(new Error("no id_token in params")); } - // Grab the tenant out of the request, as we need some more details. Coral - // is guaranteed at this point. - const { now, tenant } = req.coral!; + // Grab the tenant out of the request, as we need some more details. + const { now, tenant } = req.coral; if (!tenant) { // TODO: return a better error. return done(new Error("tenant not found")); @@ -416,12 +415,8 @@ export default class OIDCStrategy extends Strategy { ); } - private lookupStrategy(req: Request): OAuth2Strategy { - const { tenant } = req.coral!; - if (!tenant) { - // TODO: return a better error. - throw new Error("tenant not found"); - } + private lookupStrategy(req: Request): OAuth2Strategy { + const { tenant } = req.coral; // Get the integration from the tenant. If needed, it will be used to create // a new strategy. @@ -445,7 +440,7 @@ export default class OIDCStrategy extends Strategy { return tenantIntegration.strategy; } - public authenticate(req: Request) { + public authenticate(req: Request) { try { // Lookup the strategy. const strategy = this.lookupStrategy(req); diff --git a/src/core/server/app/middleware/tenant.ts b/src/core/server/app/middleware/tenant.ts index ac6423fe8..5a17d9c89 100644 --- a/src/core/server/app/middleware/tenant.ts +++ b/src/core/server/app/middleware/tenant.ts @@ -29,18 +29,14 @@ export const tenantMiddleware = ({ req.coral = { id, now, + cache: { + // Attach the tenant cache to the request. + tenant: cache, + }, logger: logger.child({ context: "http", contextID: id }, true), }; } - // Set the Coral Tenant Cache on the request. - if (!req.coral.cache) { - req.coral.cache = { - // Attach the tenant cache to the request. - tenant: cache, - }; - } - // Attach the tenant to the request. const tenant = await cache.retrieveByDomain(req.hostname); if (!tenant) { diff --git a/src/core/server/app/middleware/userLimiter.ts b/src/core/server/app/middleware/userLimiter.ts index d4fb9a0e8..f5c965a54 100644 --- a/src/core/server/app/middleware/userLimiter.ts +++ b/src/core/server/app/middleware/userLimiter.ts @@ -22,8 +22,12 @@ export const userLimiterMiddleware = ({ }); return async (req, res, next) => { + if (!req.user) { + return next(); + } + limiter - .test(req, req.user!.id) + .test(req, req.user.id) .then(() => next()) .catch((err) => next(err)); }; diff --git a/src/core/server/app/router/api/install.ts b/src/core/server/app/router/api/install.ts index ee09b6f98..832484347 100644 --- a/src/core/server/app/router/api/install.ts +++ b/src/core/server/app/router/api/install.ts @@ -15,17 +15,11 @@ export function createNewInstallRouter(app: AppOptions): Router { // Create a router. const router = createAPIRouter(); - router.get( - "/", - tenantMiddleware({ cache: app.tenantCache, passNoTenant: true }), - installCheckHandler(app) - ); - router.post( - "/", - jsonMiddleware(REQUEST_MAX), - tenantMiddleware({ cache: app.tenantCache, passNoTenant: true }), - installHandler(app) - ); + // Allow the tenant to be passed on installations. + router.use(tenantMiddleware({ cache: app.tenantCache, passNoTenant: true })); + + router.get("/", installCheckHandler(app)); + router.post("/", jsonMiddleware(REQUEST_MAX), installHandler(app)); return router; } diff --git a/src/core/server/app/router/api/story.ts b/src/core/server/app/router/api/story.ts index cdd6fd1c2..dc3d88674 100644 --- a/src/core/server/app/router/api/story.ts +++ b/src/core/server/app/router/api/story.ts @@ -1,5 +1,5 @@ import { AppOptions } from "coral-server/app"; -import { countHandler } from "coral-server/app/handlers"; +import { activeHandler, countHandler } from "coral-server/app/handlers"; import { createAPIRouter } from "./helpers"; @@ -8,6 +8,7 @@ export function createStoryRouter(app: AppOptions) { const router = createAPIRouter({ cache: "2m" }); router.get("/count.js", countHandler(app)); + router.get("/active.js", activeHandler(app)); return router; } diff --git a/src/core/server/app/router/client.ts b/src/core/server/app/router/client.ts index 30e0fb471..2c118ae40 100644 --- a/src/core/server/app/router/client.ts +++ b/src/core/server/app/router/client.ts @@ -115,7 +115,7 @@ const clientHandler = ({ // Grab the locale code from the tenant configuration, if available. let locale: LanguageCode = defaultLocale; - if (req.coral && req.coral.tenant) { + if (req.coral.tenant) { locale = req.coral.tenant.locale; } @@ -155,6 +155,7 @@ export function mountClientRoutes( ); return; } + // Tenant identification middleware. router.use( tenantMiddleware({ diff --git a/src/core/server/models/comment/metrics.ts b/src/core/server/models/comment/metrics.ts index 79a6b0885..430d9922c 100644 --- a/src/core/server/models/comment/metrics.ts +++ b/src/core/server/models/comment/metrics.ts @@ -15,6 +15,8 @@ import { GQLTAG, } from "coral-server/graph/schema/__generated__/types"; +import { PUBLISHED_STATUSES } from "./constants"; + export async function retrieveHourlyCommentMetrics( mongo: Db, tenantID: string, @@ -145,20 +147,26 @@ export async function retrieveAverageCommentsMetric( return Math.floor(total / hours); } -export async function retrieveTodayTopStoryMetrics( +export async function retrieveTopStoryMetrics( mongo: Db, tenantID: string, siteID: string, - timezone: string, + limit: number, + start: Date, now: Date ) { - const start = DateTime.fromJSDate(now).setZone(timezone).startOf("day"); - const end = DateTime.fromJSDate(now); - - // Return the last 24 hours worth of comments. const results = await collection(mongo) .aggregate([ - { $match: { tenantID, siteID, createdAt: { $gte: start, $lte: end } } }, + { + $match: { + tenantID, + siteID, + createdAt: { $gte: start, $lte: now }, + status: { + $in: PUBLISHED_STATUSES, + }, + }, + }, { $group: { _id: "$storyID", @@ -166,10 +174,26 @@ export async function retrieveTodayTopStoryMetrics( }, }, { $sort: { count: -1 } }, - // TODO: 17 was for visual treatment, feel free to change this! - { $limit: 17 }, + { $limit: limit }, ]) .toArray(); return results; } + +export async function retrieveTodayTopStoryMetrics( + mongo: Db, + tenantID: string, + siteID: string, + timezone: string, + limit: number, + now: Date +) { + // Return the last day worth of comments. + const start = DateTime.fromJSDate(now) + .setZone(timezone) + .startOf("day") + .toJSDate(); + + return retrieveTopStoryMetrics(mongo, tenantID, siteID, limit, start, now); +} diff --git a/src/core/server/types/express.ts b/src/core/server/types/express.ts index 879d2ec2e..e6ca43233 100644 --- a/src/core/server/types/express.ts +++ b/src/core/server/types/express.ts @@ -1,5 +1,6 @@ import { NextFunction, Request as ExpressRequest, Response } from "express"; +import { RequireProperty } from "coral-common/types"; import { Logger } from "coral-server/logger"; import { PersistedQuery } from "coral-server/models/queries"; import { Tenant } from "coral-server/models/tenant"; @@ -9,7 +10,7 @@ import { TenantCache } from "coral-server/services/tenant/cache"; export interface CoralRequest { id: string; now: Date; - cache?: { + cache: { tenant: TenantCache; }; tenant?: Tenant; @@ -17,20 +18,22 @@ export interface CoralRequest { logger: Logger; } -export interface Request extends ExpressRequest { - coral?: CoralRequest; +export type TenantCoralRequest = RequireProperty; + +export interface Request extends ExpressRequest { + coral: T; user?: User; } -export type RequestHandler = ( - req: Request, +export type RequestHandler = ( + req: Request, res: Response, next: NextFunction ) => void; export type ErrorRequestHandler = ( err: Error, - req: Request, + req: Request, res: Response, next: NextFunction ) => void; diff --git a/src/index.ts b/src/index.ts index 86128b860..983ac57eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,7 @@ import dotenv from "dotenv"; import { rewrite } from "env-rewrite"; -import express from "express"; import sourceMapSupport from "source-map-support"; -import createCoral from "./core"; -import logger from "./core/server/logger"; - // Configure the source map support so stack traces will reference the source // files rather than the transpiled code. sourceMapSupport.install({ @@ -25,6 +21,14 @@ rewrite(); // the environment. dotenv.config(); +// NOTE: It is required for the `dotenv` module to be configured before other +// modules to ensure the rewriting takes place before those modules load! + +import express from "express"; + +import createCoral from "./core"; +import logger from "./core/server/logger"; + // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will // terminate the Node.js process with a non-zero exit code.