mirror of
https://github.com/wassname/talk.git
synced 2026-07-22 13:00:29 +08:00
[CORL-1149] Active Stories (#3040)
* fix: removed type assertions in place of stricter types * fix: fixed bug with module ordering * feat: support for story activity jsonp route
This commit is contained in:
@@ -71,9 +71,9 @@ function detectAndInject(opts: DetectAndInjectArgs = {}) {
|
||||
const { url, id, notext } = queryMap[ref];
|
||||
|
||||
// Compile the arguments used to generate the
|
||||
const args: Record<string, string | number | undefined> = {
|
||||
url,
|
||||
const args: Record<string, string | undefined> = {
|
||||
id,
|
||||
url,
|
||||
notext: notext ? "true" : "false",
|
||||
ref,
|
||||
};
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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<User>,
|
||||
coral.now
|
||||
now
|
||||
);
|
||||
|
||||
log.trace("sent confirm email with token");
|
||||
@@ -144,7 +142,7 @@ export const confirmCheckHandler = ({
|
||||
mongo,
|
||||
signingConfig,
|
||||
config,
|
||||
}: ConfirmCheckOptions): RequestHandler => {
|
||||
}: ConfirmCheckOptions): RequestHandler<TenantCoralRequest> => {
|
||||
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<TenantCoralRequest> => {
|
||||
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) {
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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<TenantCoralRequest> => {
|
||||
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);
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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<TenantCoralRequest> => {
|
||||
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);
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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<TenantCoralRequest> => {
|
||||
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.
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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<TenantCoralRequest> => {
|
||||
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<TenantCoralRequest> => {
|
||||
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);
|
||||
|
||||
@@ -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<AppOptions, "redis">;
|
||||
|
||||
export const logoutHandler = ({
|
||||
redis,
|
||||
}: LogoutOptions): RequestHandler => async (req, res, next) => {
|
||||
}: LogoutOptions): RequestHandler<TenantCoralRequest> => async (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
try {
|
||||
// Get the user on the request.
|
||||
const user = req.user;
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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) {
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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")) {
|
||||
|
||||
@@ -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<TenantCoralRequest>) {
|
||||
// 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<TenantCoralRequest> => 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<TenantCoralRequest> => 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<TenantCoralRequest> => 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<TenantCoralRequest> => 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<TenantCoralRequest> => 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
|
||||
);
|
||||
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
// 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<TenantCoralRequest>) => {
|
||||
// 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 = {
|
||||
|
||||
@@ -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<CoralRequest> => {
|
||||
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
|
||||
|
||||
@@ -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<AppOptions, "i18n">;
|
||||
|
||||
export const oembedHandler = ({ i18n }: OembedHandler): RequestHandler => {
|
||||
export const oembedHandler = ({
|
||||
i18n,
|
||||
}: OembedHandler): RequestHandler<TenantCoralRequest> => {
|
||||
// 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(
|
||||
|
||||
@@ -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<TenantCoralRequest> = 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,
|
||||
|
||||
@@ -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<AppOptions, "mongo">;
|
||||
|
||||
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<TenantCoralRequest> => 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);
|
||||
}
|
||||
};
|
||||
@@ -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<AppOptions, "mongo" | "tenantCache" | "i18n">;
|
||||
|
||||
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<TenantCoralRequest> => 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 = `<span class="${NUMBER_CLASS_NAME}">${count}</span>`;
|
||||
} 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) {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./active";
|
||||
export * from "./count";
|
||||
|
||||
@@ -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<TenantCoralRequest> => {
|
||||
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.
|
||||
|
||||
@@ -20,16 +20,12 @@ async function retrieveSiteFromEmbed(
|
||||
mongo: Db,
|
||||
req: Request
|
||||
): Promise<Site | null> {
|
||||
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 = "",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<TenantCoralRequest>,
|
||||
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<TenantCoralRequest>,
|
||||
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<TenantCoralRequest>,
|
||||
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<TenantCoralRequest> => (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<TenantCoralRequest> => (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<TenantCoralRequest> => (req, res, next) =>
|
||||
authenticator.authenticate(
|
||||
"jwt",
|
||||
{ session: false },
|
||||
|
||||
@@ -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<TenantCoralRequest>) {
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -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<TenantCoralRequest>,
|
||||
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, {
|
||||
|
||||
@@ -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<User | null | undefined>;
|
||||
|
||||
protected verifyCallback = async (
|
||||
req: Request,
|
||||
req: Request<TenantCoralRequest>,
|
||||
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<T>,
|
||||
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<TenantCoralRequest>) {
|
||||
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);
|
||||
|
||||
@@ -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<TenantCoralRequest>): 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<TenantCoralRequest>) {
|
||||
try {
|
||||
// Lookup the strategy.
|
||||
const strategy = this.lookupStrategy(req);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<Result>(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);
|
||||
}
|
||||
|
||||
@@ -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<CoralRequest, "tenant">;
|
||||
|
||||
export interface Request<T = CoralRequest> extends ExpressRequest {
|
||||
coral: T;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export type RequestHandler = (
|
||||
req: Request,
|
||||
export type RequestHandler<T = CoralRequest> = (
|
||||
req: Request<T>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => void;
|
||||
|
||||
export type ErrorRequestHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
req: Request<CoralRequest>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => void;
|
||||
|
||||
+8
-4
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user