Merge branch 'next' into next-static-uri

This commit is contained in:
Wyatt Johnson
2018-10-15 22:47:04 +00:00
committed by GitHub
27 changed files with 646 additions and 186 deletions
@@ -43,7 +43,7 @@ export const signupHandler = (options: SignupOptions): RequestHandler => async (
// TODO: rate limit based on the IP address and user agent.
// Tenant is guaranteed at this point.
const tenant = req.tenant!;
const tenant = req.talk!.tenant!;
// Check to ensure that the local integration has been enabled.
if (!tenant.auth.integrations.local.enabled) {
@@ -95,7 +95,7 @@ export const logoutHandler = (options: LogoutOptions): RequestHandler => async (
// TODO: rate limit based on the IP address and user agent.
// Tenant is guaranteed at this point.
const tenant = req.tenant!;
const tenant = req.talk!.tenant!;
// Check to ensure that the local integration has been enabled.
if (!tenant.auth.integrations.local.enabled) {
@@ -0,0 +1,51 @@
import { RequestHandler } from "express-jwt";
import { Redis } from "ioredis";
import { Db } from "mongodb";
import TenantContext from "talk-server/graph/tenant/context";
import { TaskQueue } from "talk-server/services/queue";
import { Request } from "talk-server/types/express";
export interface TenantContextMiddlewareOptions {
mongo: Db;
redis: Redis;
queue: TaskQueue;
}
export const tenantContext = ({
mongo,
redis,
queue,
}: TenantContextMiddlewareOptions): RequestHandler => (
req: Request,
res,
next
) => {
if (!req.talk) {
return next(new Error("talk was not set"));
}
const { tenant, cache } = req.talk;
if (!cache) {
return next(new Error("cache was not set"));
}
if (!tenant) {
return next(new Error("tenant was not set"));
}
req.talk.context = {
tenant: new TenantContext({
req,
mongo,
redis,
tenant,
user: req.user,
tenantCache: cache.tenant,
queue,
}),
};
next();
};
@@ -0,0 +1,97 @@
import { RequestHandler, Response } from "express";
import logger from "talk-server/logger";
import { Request } from "talk-server/types/express";
function wrapResponse(req: Request, res: Response) {
// If the request is not an array, or has no elements, we should skip it.
if (!Array.isArray(req.body) || req.body.length === 0) {
return res;
}
// If the request is an array, but it does not have an ID field, then we
// should skip it.
const needsUpgrade = Boolean(typeof req.body[0].id !== "undefined");
if (!needsUpgrade) {
return res;
}
// Grab all the existing ID's.
const ids: string[] = req.body.map(({ id }) => id);
// Save a reference to the old setHeader function.
const setHeader = res.setHeader.bind(res);
// Capture all the headers that are sent to this, in case we need to use it.
const setHeaders: Record<string, any> = {};
res.setHeader = (name: string, value: any) => {
setHeaders[name] = value;
return res;
};
// Save a reference to the old write function.
const write = res.write.bind(res);
// Create a flush function that will be used to flush the response to the
// underlying response.
const flush = (chunk: any, headers: Record<string, any> = setHeaders) => {
for (const name in headers) {
if (!headers.hasOwnProperty(name)) {
continue;
}
setHeader(name, headers[name]);
}
return write(chunk);
};
// Override the response writer to parse the response to determine if it needs
// to be rewritten.
res.write = (chunk: string) => {
try {
// If there is no response, forward it, or if we peek at the first
// character and it's not an array opening, then skip it.
if (chunk.length <= 0 || chunk[0] !== "[") {
return flush(chunk);
}
// Parse the responses, if it's not an array, then skip it.
const responses: object[] | any = JSON.parse(chunk);
if (!Array.isArray(responses) || responses.length === 0) {
return flush(chunk);
}
// If the length of responses do not equal the length of id's collected,
// then skip it.
if (responses.length !== ids.length) {
return flush(chunk);
}
// For each of the responses, zip up their id's into the objects, and
// string concat them together to ensure we get the right request.
const gqlResponse = responses.reduce((body: object[], payload, idx) => {
const id = ids[idx];
body.push({ id, payload });
return body;
}, []);
const response = JSON.stringify(gqlResponse);
return flush(response, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(response, "utf8").toString(),
});
} catch (err) {
logger.error({ err }, "could not parse chunk as JSON");
return flush(chunk);
}
};
return res;
}
export const graphqlBatchMiddleware = (
graphqlRequestHandler: RequestHandler
): RequestHandler => (req: Request, res, next) =>
graphqlRequestHandler(req, wrapResponse(req, res), next);
@@ -102,8 +102,8 @@ export async function handleSuccessfulLogin(
next: NextFunction
) {
try {
// Grab the tenant from the request.
const { tenant } = req;
// Talk is guaranteed at this point.
const { tenant } = req.talk!;
const options: SigningTokenOptions = {};
@@ -100,7 +100,7 @@ export class JWTStrategy extends Strategy {
return this.pass();
}
const { tenant } = req;
const { tenant } = req.talk!;
if (!tenant) {
// TODO: (wyattjoh) log this error, and return a better one?
return this.error(new Error("tenant not found"));
@@ -18,7 +18,7 @@ const verifyFactory = (mongo: Db) => async (
// TODO: rate limit based on the IP address and user agent.
// The tenant is guaranteed at this point.
const tenant = req.tenant!;
const tenant = req.talk!.tenant!;
// Get the user from the database.
const user = await retrieveUserWithProfile(mongo, tenant.id, {
@@ -265,7 +265,7 @@ export default class OIDCStrategy extends Strategy {
}
// Grab the tenant out of the request, as we need some more details.
const { tenant } = req;
const { tenant } = req.talk!;
if (!tenant) {
// TODO: return a better error.
return done(new Error("tenant not found"));
@@ -336,7 +336,7 @@ export default class OIDCStrategy extends Strategy {
}
private async lookupStrategy(req: Request) {
const { tenant } = req;
const { tenant } = req.talk!;
if (!tenant) {
// TODO: return a better error.
throw new Error("tenant not found");
+10 -8
View File
@@ -7,14 +7,12 @@ export interface MiddlewareOptions {
cache: TenantCache;
}
export default (options: MiddlewareOptions): RequestHandler => async (
export default ({ cache }: MiddlewareOptions): RequestHandler => async (
req: Request,
res,
next
) => {
try {
const { cache } = options;
// Attach the tenant to the request.
const tenant = await cache.retrieveByDomain(req.hostname);
if (!tenant) {
@@ -22,11 +20,15 @@ export default (options: MiddlewareOptions): RequestHandler => async (
return next(new Error("tenant not found"));
}
// Attach the tenant cache to the request.
req.tenantCache = cache;
// Attach the tenant to the request.
req.tenant = tenant;
// Set Talk on the request.
req.talk = {
cache: {
// Attach the tenant cache to the request.
tenant: cache,
},
// Attach the tenant to the request.
tenant,
};
// Attach the tenant to the view locals.
res.locals.tenant = tenant;
+6 -3
View File
@@ -6,6 +6,7 @@ import tenantMiddleware from "talk-server/app/middleware/tenant";
import { RouterOptions } from "talk-server/app/router/types";
import tenantGraphMiddleware from "talk-server/graph/tenant/middleware";
import { tenantContext } from "talk-server/app/middleware/context/tenant";
import { createNewAuthRouter } from "./auth";
export async function createTenantRouter(
@@ -41,12 +42,14 @@ export async function createTenantRouter(
// Any users may submit their GraphQL requests with authentication, this
// middleware will unpack their user into the request.
options.passport.authenticate("jwt", { session: false }),
await tenantGraphMiddleware({
schema: app.schemas.tenant,
config: app.config,
tenantContext({
mongo: app.mongo,
redis: app.redis,
queue: app.queue,
}),
await tenantGraphMiddleware({
schema: app.schemas.tenant,
config: app.config,
})
);
+25 -33
View File
@@ -1,45 +1,37 @@
import { GraphQLSchema } from "graphql";
import { Redis } from "ioredis";
import { Db } from "mongodb";
// import { graphqlBatchHTTPWrapper } from "react-relay-network-layer";
import { Config } from "talk-common/config";
import { graphqlBatchMiddleware } from "talk-server/app/middleware/graphqlBatch";
import { graphqlMiddleware } from "talk-server/graph/common/middleware";
import { TaskQueue } from "talk-server/services/queue";
import { Request } from "talk-server/types/express";
import TenantContext from "./context";
export interface TenantGraphQLMiddlewareOptions {
schema: GraphQLSchema;
config: Config;
mongo: Db;
redis: Redis;
queue: TaskQueue;
}
export default async ({
schema,
config,
mongo,
redis,
queue,
}: TenantGraphQLMiddlewareOptions) => {
return graphqlMiddleware(config, async (req: Request) => {
// Load the tenant and user from the request.
const { tenant, user, tenantCache } = req;
export default async ({ schema, config }: TenantGraphQLMiddlewareOptions) =>
graphqlBatchMiddleware(
graphqlMiddleware(config, async (req: Request) => {
if (!req.talk) {
throw new Error("talk was not set");
}
// Return the graph options.
return {
schema,
context: new TenantContext({
req,
mongo,
redis,
tenant: tenant!,
user,
tenantCache,
queue,
}),
};
});
};
const { context } = req.talk;
if (!context) {
throw new Error("context was not set");
}
const { tenant } = context;
if (!tenant) {
throw new Error("tenant was not set");
}
// Return the graph options.
return {
schema,
context: tenant,
};
})
);
+13 -3
View File
@@ -1,11 +1,21 @@
import { Request } from "express";
import TenantContext from "talk-server/graph/tenant/context";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
import TenantCache from "talk-server/services/tenant/cache";
export interface Request extends Request {
user?: User;
export interface TalkRequest {
cache?: {
tenant: TenantCache;
};
tenant?: Tenant;
tenantCache: TenantCache;
context?: {
tenant?: TenantContext;
};
}
export interface Request extends Request {
talk?: TalkRequest;
user?: User;
}