[CORL 133] API Review (#2197)

* refactor: removed unused subscription code

* refactor: removed management api's

* refactor: cleanup of connections

* refactor: refactored comments edge

* refactor: simplified connection resolving

* feat: added story connection edge

* fix: added story index

* feat: added user pagination and user edge

* fix: added filter to comment query

* fix: removed unused resolvers

* fix: creating a comment reply should require auth

* refactor: cleanup of graph files

* feat: removed display name, made username non-unique

* fix: fixed tests

* fix: fixed tests

* fix: added more api docs

* fix: fixed bug with installer

* refactor: fixes and updates

* fix: added linting for graphql, fixed schema

* feat: added docker build tests

* fix: upped output timeout

* fix: fixed stacktraces in production builds

* fix: removed `git add`

- `git add` was causing issues with
    partial staged changs on files

* feat: improved error messaging for auth

* refactor: cleaned up queue names

* fix: merge error
This commit is contained in:
Wyatt Johnson
2019-03-12 15:12:21 +01:00
committed by Kiwi
parent 37959f9398
commit d37333be89
125 changed files with 1269 additions and 1536 deletions
@@ -1,8 +1,7 @@
import { RequestHandler } from "express";
import { Redis } from "ioredis";
import Joi from "joi";
import { Db } from "mongodb";
import { AppOptions } from "talk-server/app";
import {
handleLogout,
handleSuccessfulLogin,
@@ -10,7 +9,6 @@ import {
import { validate } from "talk-server/app/request/body";
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
import { LocalProfile } from "talk-server/models/user";
import { JWTSigningConfig } from "talk-server/services/jwt";
import { upsert } from "talk-server/services/users";
import { Request } from "talk-server/types/express";
@@ -29,16 +27,12 @@ export const SignupBodySchema = Joi.object().keys({
.email(),
});
export interface SignupOptions {
db: Db;
signingConfig: JWTSigningConfig;
}
export type SignupOptions = Pick<AppOptions, "mongo" | "signingConfig">;
export const signupHandler = (options: SignupOptions): RequestHandler => async (
req: Request,
res,
next
) => {
export const signupHandler = ({
mongo,
signingConfig,
}: SignupOptions): RequestHandler => async (req: Request, res, next) => {
try {
// TODO: rate limit based on the IP address and user agent.
@@ -71,7 +65,7 @@ export const signupHandler = (options: SignupOptions): RequestHandler => async (
};
// Create the new user.
const user = await upsert(options.db, tenant, {
const user = await upsert(mongo, tenant, {
email,
username,
profiles: [profile],
@@ -81,21 +75,17 @@ export const signupHandler = (options: SignupOptions): RequestHandler => async (
});
// Send off to the passport handler.
return handleSuccessfulLogin(user, options.signingConfig, req, res, next);
return handleSuccessfulLogin(user, signingConfig, req, res, next);
} catch (err) {
return next(err);
}
};
export interface LogoutOptions {
redis: Redis;
}
export type LogoutOptions = Pick<AppOptions, "redis">;
export const logoutHandler = (options: LogoutOptions): RequestHandler => async (
req: Request,
res,
next
) => {
export const logoutHandler = ({
redis,
}: LogoutOptions): RequestHandler => async (req: Request, res, next) => {
try {
// TODO: rate limit based on the IP address and user agent.
@@ -117,7 +107,7 @@ export const logoutHandler = (options: LogoutOptions): RequestHandler => async (
}
// Delegate to the logout handler.
return handleLogout(options.redis, req, res);
return handleLogout(redis, req, res);
} catch (err) {
return next(err);
}
@@ -0,0 +1,54 @@
import { AppOptions } from "talk-server/app";
import {
graphqlBatchMiddleware,
graphqlMiddleware,
} from "talk-server/app/middleware/graphql";
import TenantContext from "talk-server/graph/tenant/context";
import { Request, RequestHandler } from "talk-server/types/express";
export type GraphMiddlewareOptions = Pick<
AppOptions,
| "schema"
| "config"
| "mongo"
| "redis"
| "mailerQueue"
| "scraperQueue"
| "signingConfig"
| "i18n"
>;
export const graphQLHandler = ({
schema,
config,
...options
}: GraphMiddlewareOptions): RequestHandler =>
graphqlBatchMiddleware(
graphqlMiddleware(config, async (req: Request) => {
if (!req.talk) {
throw new Error("talk was not set");
}
const { tenant, cache } = req.talk;
if (!cache) {
throw new Error("cache was not set");
}
if (!tenant) {
throw new Error("tenant was not set");
}
return {
schema,
context: new TenantContext({
...options,
req,
config,
tenant,
user: req.user,
tenantCache: cache.tenant,
}),
};
})
);
@@ -1,4 +1,3 @@
import { RequestHandler } from "express";
import { Redis } from "ioredis";
import Joi from "joi";
import { Db } from "mongodb";
@@ -7,12 +6,12 @@ import { LanguageCode, LOCALES } from "talk-common/helpers/i18n/locales";
import { Omit } from "talk-common/types";
import { validate } from "talk-server/app/request/body";
import { Config } from "talk-server/config";
import { TenantInstalledAlreadyError } from "talk-server/errors";
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
import { LocalProfile } from "talk-server/models/user";
import { install, InstallTenant } from "talk-server/services/tenant";
import TenantCache from "talk-server/services/tenant/cache";
import { upsert, UpsertUser } from "talk-server/services/users";
import { Request } from "talk-server/types/express";
import { RequestHandler } from "talk-server/types/express";
export interface TenantInstallBody {
tenant: Omit<InstallTenant, "domain" | "locale"> & {
@@ -53,23 +52,30 @@ const TenantInstallBodySchema = Joi.object().keys({
});
export interface TenantInstallHandlerOptions {
cache: TenantCache;
redis: Redis;
mongo: Db;
config: Config;
}
export const tenantInstallHandler = ({
export const installHandler = ({
mongo,
redis,
cache,
config,
}: TenantInstallHandlerOptions): RequestHandler => async (
req: Request,
res,
next
) => {
}: TenantInstallHandlerOptions): RequestHandler => async (req, res, next) => {
try {
if (!req.talk) {
return next(new Error("talk was not set"));
}
if (!req.talk.cache) {
return next(new Error("cache was not set"));
}
if (req.talk.tenant) {
// There's already a Tenant on the request! No need to process further.
return next(new TenantInstalledAlreadyError());
}
// Validate that the payload passed in was correct, it will throw if the
// payload is invalid.
const {
@@ -85,7 +91,7 @@ export const tenantInstallHandler = ({
// Install will throw if it can not create a Tenant, or it has already been
// installed.
const tenant = await install(mongo, redis, cache, {
const tenant = await install(mongo, redis, req.talk.cache.tenant, {
...tenantInput,
// Infer the Tenant domain via the hostname parameter.
domain: req.hostname,
+9 -37
View File
@@ -1,6 +1,7 @@
import cons from "consolidate";
import cors from "cors";
import { Express } from "express";
import { GraphQLSchema } from "graphql";
import http from "http";
import { Db } from "mongodb";
import nunjucks from "nunjucks";
@@ -11,9 +12,8 @@ import { HTMLErrorHandler } from "talk-server/app/middleware/error";
import { notFoundMiddleware } from "talk-server/app/middleware/notFound";
import { createPassport } from "talk-server/app/middleware/passport";
import { Config } from "talk-server/config";
import { handleSubscriptions } from "talk-server/graph/common/subscriptions/middleware";
import { Schemas } from "talk-server/graph/schemas";
import { TaskQueue } from "talk-server/queue";
import { MailerQueue } from "talk-server/queue/tasks/mailer";
import { ScraperQueue } from "talk-server/queue/tasks/scraper";
import { I18n } from "talk-server/services/i18n";
import { JWTSigningConfig } from "talk-server/services/jwt";
import { AugmentedRedis } from "talk-server/services/redis";
@@ -24,15 +24,16 @@ import serveStatic from "./middleware/serveStatic";
import { createRouter } from "./router";
export interface AppOptions {
parent: Express;
queue: TaskQueue;
config: Config;
i18n: I18n;
mailerQueue: MailerQueue;
scraperQueue: ScraperQueue;
mongo: Db;
parent: Express;
redis: AugmentedRedis;
schemas: Schemas;
schema: GraphQLSchema;
signingConfig: JWTSigningConfig;
tenantCache: TenantCache;
i18n: I18n;
}
/**
@@ -52,12 +53,7 @@ export async function createApp(options: AppOptions): Promise<Express> {
const passport = createPassport(options);
// Mount the router.
parent.use(
"/",
await createRouter(options, {
passport,
})
);
parent.use("/", createRouter(options, { passport }));
// Enable CORS headers for media assets, font's require them.
parent.use("/assets/media", cors());
@@ -120,27 +116,3 @@ function setupViews(options: AppOptions) {
// set .html as the default extension.
parent.set("view engine", "html");
}
/**
* attachSubscriptionHandlers attaches all the handlers to the http.Server to
* handle websocket traffic by upgrading their http connections to websocket.
*
* @param schemas schemas for every schema this application handles
* @param server the http.Server to attach the websocket upgrader to
*/
export async function attachSubscriptionHandlers(
schemas: Schemas,
server: http.Server
) {
// Setup the Management Subscription endpoint.
handleSubscriptions(server, {
schema: schemas.management,
path: "/api/management/live",
});
// Setup the Tenant Subscription endpoint.
handleSubscriptions(server, {
schema: schemas.tenant,
path: "/api/tenant/live",
});
}
@@ -1,63 +0,0 @@
import { RequestHandler } from "express-jwt";
import { Db } from "mongodb";
import { Config } from "talk-server/config";
import TenantContext from "talk-server/graph/tenant/context";
import { TaskQueue } from "talk-server/queue";
import { I18n } from "talk-server/services/i18n";
import { JWTSigningConfig } from "talk-server/services/jwt";
import { AugmentedRedis } from "talk-server/services/redis";
import { Request } from "talk-server/types/express";
export interface TenantContextMiddlewareOptions {
mongo: Db;
redis: AugmentedRedis;
queue: TaskQueue;
config: Config;
signingConfig: JWTSigningConfig;
i18n: I18n;
}
export const tenantContext = ({
mongo,
redis,
queue,
config,
signingConfig,
i18n,
}: 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,
config,
mongo,
redis,
tenant,
user: req.user,
tenantCache: cache.tenant,
queue,
signingConfig,
i18n,
}),
};
next();
};
@@ -0,0 +1,72 @@
import { GraphQLOptions } from "apollo-server-express";
import { Handler } from "express";
import { FieldDefinitionNode, GraphQLError, ValidationContext } from "graphql";
// TODO: when https://github.com/apollographql/apollo-server/pull/1907 is merged, update this import path
import {
ExpressGraphQLOptionsFunction,
graphqlExpress,
} from "apollo-server-express/dist/expressApollo";
import { Omit } from "talk-common/types";
import { Config } from "talk-server/config";
import {
ErrorWrappingExtension,
LoggerExtension,
} from "talk-server/graph/common/extensions";
export * from "./batch";
// Sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L46-L57
const NoIntrospection = (context: ValidationContext) => ({
Field(node: FieldDefinitionNode) {
if (node.name.value === "__schema" || node.name.value === "__type") {
context.reportError(
new GraphQLError(
"GraphQL introspection is not allowed in production, but the query contained __schema or __type.",
[node]
)
);
}
},
});
/**
* graphqlMiddleware wraps the GraphQL middleware server with some custom
* extension management.
*
* @param config application configuration
* @param requestOptions options to pass to the graphql server
*/
export const graphqlMiddleware = (
config: Config,
requestOptions: ExpressGraphQLOptionsFunction
): Handler => {
// Create a new baseOptions that will be merged into the new options.
const baseOptions: Omit<GraphQLOptions, "schema"> = {
// Disable the debug mode, as we already add in our logging function.
debug: false,
// Include extensions.
extensions: [
() => new ErrorWrappingExtension(),
() => new LoggerExtension(),
],
};
if (config.get("env") === "production" && !config.get("enable_graphiql")) {
// Disable introspection in production.
baseOptions.validationRules = [NoIntrospection];
}
// Generate the actual middleware.
return graphqlExpress(async (req, res) => {
// Resolve the options for the GraphQL middleware.
const options = await requestOptions(req, res);
// Provide the options.
return {
...options,
...baseOptions,
};
});
};
+24 -9
View File
@@ -1,20 +1,35 @@
import { RequestHandler } from "express";
import { isInstalled } from "talk-server/services/tenant";
import TenantCache from "talk-server/services/tenant/cache";
import { RequestHandler } from "talk-server/types/express";
export interface InstalledMiddlewareOptions {
tenantCache: TenantCache;
redirectURL?: string;
redirectIfInstalled?: boolean;
}
const DefaultInstalledMiddlewareOptions: Required<
InstalledMiddlewareOptions
> = {
redirectIfInstalled: false,
redirectURL: "/install",
};
export const installedMiddleware = ({
tenantCache,
redirectIfInstalled = false,
redirectURL = "/install",
}: InstalledMiddlewareOptions): RequestHandler => async (req, res, next) => {
const installed = await isInstalled(tenantCache);
redirectIfInstalled = DefaultInstalledMiddlewareOptions.redirectIfInstalled,
redirectURL = DefaultInstalledMiddlewareOptions.redirectURL,
}: InstalledMiddlewareOptions = DefaultInstalledMiddlewareOptions): RequestHandler => async (
req,
res,
next
) => {
if (!req.talk) {
return next(new Error("talk was not set"));
}
if (!req.talk.cache) {
return next(new Error("cache was not set"));
}
const installed = await isInstalled(req.talk.cache.tenant);
// If Talk is installed, and redirectIfInstall is true, then it will redirect.
// If Talk is not installed, and redirectIfInstall is false, then it will also
@@ -2,17 +2,16 @@ import { NextFunction, RequestHandler, Response } from "express";
import { Redis } from "ioredis";
import Joi from "joi";
import jwt from "jsonwebtoken";
import { Db } from "mongodb";
import passport, { Authenticator } from "passport";
import now from "performance-now";
import { AppOptions } from "talk-server/app";
import FacebookStrategy from "talk-server/app/middleware/passport/strategies/facebook";
import GoogleStrategy from "talk-server/app/middleware/passport/strategies/google";
import { JWTStrategy } from "talk-server/app/middleware/passport/strategies/jwt";
import { createLocalStrategy } from "talk-server/app/middleware/passport/strategies/local";
import OIDCStrategy from "talk-server/app/middleware/passport/strategies/oidc";
import { validate } from "talk-server/app/request/body";
import { Config } from "talk-server/config";
import logger from "talk-server/logger";
import { User } from "talk-server/models/user";
import {
@@ -22,7 +21,6 @@ import {
SigningTokenOptions,
signTokenString,
} from "talk-server/services/jwt";
import TenantCache from "talk-server/services/tenant/cache";
import { Request } from "talk-server/types/express";
export type VerifyCallback = (
@@ -31,13 +29,10 @@ export type VerifyCallback = (
info?: { message: string }
) => void;
export interface PassportOptions {
config: Config;
mongo: Db;
redis: Redis;
signingConfig: JWTSigningConfig;
tenantCache: TenantCache;
}
export type PassportOptions = Pick<
AppOptions,
"mongo" | "redis" | "config" | "tenantCache" | "signingConfig"
>;
export function createPassport(
options: PassportOptions
@@ -142,8 +137,7 @@ export async function handleOAuth2Callback(
user: User | null,
signingConfig: JWTSigningConfig,
req: Request,
res: Response,
next: NextFunction
res: Response
) {
const path = "/embed/auth/callback";
if (!user) {
@@ -195,7 +189,7 @@ export const wrapOAuth2Authn = (
name,
{ ...options, session: false },
(err: Error | null, user: User | null) => {
handleOAuth2Callback(err, user, signingConfig, req, res, next);
handleOAuth2Callback(err, user, signingConfig, req, res);
}
)(req, res, next);
@@ -1,9 +1,9 @@
import { Db } from "mongodb";
import { Profile, Strategy } from "passport-facebook";
import OAuth2Strategy from "talk-server/app/middleware/passport/strategies/oauth2";
import OAuth2Strategy, {
OAuth2StrategyOptions,
} from "talk-server/app/middleware/passport/strategies/oauth2";
import { constructTenantURL } from "talk-server/app/url";
import { Config } from "talk-server/config";
import {
GQLAuthIntegrations,
GQLFacebookAuthIntegration,
@@ -14,14 +14,9 @@ import {
FacebookProfile,
retrieveUserWithProfile,
} from "talk-server/models/user";
import TenantCache from "talk-server/services/tenant/cache";
import { upsert } from "talk-server/services/users";
export interface FacebookStrategyOptions {
config: Config;
mongo: Db;
tenantCache: TenantCache;
}
export type FacebookStrategyOptions = OAuth2StrategyOptions;
export default class FacebookStrategy extends OAuth2Strategy<
GQLFacebookAuthIntegration,
@@ -77,7 +72,7 @@ export default class FacebookStrategy extends OAuth2Strategy<
}
user = await upsert(this.mongo, tenant, {
displayName,
username: displayName,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified,
@@ -102,7 +97,7 @@ export default class FacebookStrategy extends OAuth2Strategy<
callbackURL: constructTenantURL(
this.config,
tenant,
"/api/tenant/auth/facebook/callback"
"/api/auth/facebook/callback"
),
profileFields: ["id", "displayName", "photos", "email"],
enableProof: true,
@@ -1,9 +1,9 @@
import { Db } from "mongodb";
import { Profile, Strategy } from "passport-google-oauth2";
import OAuth2Strategy from "talk-server/app/middleware/passport/strategies/oauth2";
import OAuth2Strategy, {
OAuth2StrategyOptions,
} from "talk-server/app/middleware/passport/strategies/oauth2";
import { constructTenantURL } from "talk-server/app/url";
import { Config } from "talk-server/config";
import {
GQLAuthIntegrations,
GQLGoogleAuthIntegration,
@@ -14,20 +14,9 @@ import {
GoogleProfile,
retrieveUserWithProfile,
} from "talk-server/models/user";
import TenantCache from "talk-server/services/tenant/cache";
import { upsert } from "talk-server/services/users";
export interface GoogleStrategyOptions {
config: Config;
mongo: Db;
tenantCache: TenantCache;
}
export interface GoogleStrategyOptions {
config: Config;
mongo: Db;
tenantCache: TenantCache;
}
export type GoogleStrategyOptions = OAuth2StrategyOptions;
export default class GoogleStrategy extends OAuth2Strategy<
GQLGoogleAuthIntegration,
@@ -82,7 +71,7 @@ export default class GoogleStrategy extends OAuth2Strategy<
}
user = await upsert(this.mongo, tenant, {
displayName,
username: displayName,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified,
@@ -107,7 +96,7 @@ export default class GoogleStrategy extends OAuth2Strategy<
callbackURL: constructTenantURL(
this.config,
tenant,
"/api/tenant/auth/google/callback"
"/api/auth/google/callback"
),
passReqToCallback: true,
},
@@ -1,8 +1,7 @@
import { Redis } from "ioredis";
import jwt from "jsonwebtoken";
import { Db } from "mongodb";
import { Strategy } from "passport-strategy";
import { AppOptions } from "talk-server/app";
import {
JWTToken,
JWTVerifier,
@@ -14,17 +13,13 @@ import {
import { TenantNotFoundError, TokenInvalidError } from "talk-server/errors";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
import {
extractJWTFromRequest,
JWTSigningConfig,
} from "talk-server/services/jwt";
import { extractJWTFromRequest } from "talk-server/services/jwt";
import { Request } from "talk-server/types/express";
export interface JWTStrategyOptions {
signingConfig: JWTSigningConfig;
mongo: Db;
redis: Redis;
}
export type JWTStrategyOptions = Pick<
AppOptions,
"signingConfig" | "mongo" | "redis"
>;
/**
* Token is the various forms of the Token that can be verified.
@@ -37,6 +37,7 @@ export interface OIDCIDToken {
picture?: string;
name?: string;
nickname?: string;
preferred_username?: string;
}
export interface StrategyItem {
@@ -121,11 +122,18 @@ export const OIDCIDTokenSchema = Joi.object()
picture: Joi.string().default(undefined),
name: Joi.string().default(undefined),
nickname: Joi.string().default(undefined),
preferred_username: Joi.string().default(undefined),
})
.optionalKeys(["picture", "email_verified", "name", "nickname"]);
.optionalKeys([
"picture",
"email_verified",
"name",
"nickname",
"preferred_username",
]);
export async function findOrCreateOIDCUser(
db: Db,
mongo: Db,
tenant: Tenant,
integration: GQLOIDCAuthIntegration,
token: OIDCIDToken
@@ -140,6 +148,7 @@ export async function findOrCreateOIDCUser(
picture,
name,
nickname,
preferred_username,
}: OIDCIDToken = validate(OIDCIDTokenSchema, token);
// Construct the profile that will be used to query for the user.
@@ -151,7 +160,7 @@ export async function findOrCreateOIDCUser(
};
// Try to lookup user given their id provided in the `sub` claim.
let user = await retrieveUserWithProfile(db, tenant.id, {
let user = await retrieveUserWithProfile(mongo, tenant.id, {
// NOTE: (wyattjoh) as the current requirements do not allow multiple OIDC integrations, we are only getting the profile based on the OIDC provider.
type: "oidc",
id: sub,
@@ -164,11 +173,12 @@ export async function findOrCreateOIDCUser(
// FIXME: implement rules.
const displayName = nickname || name || undefined;
// Try to extract the username from the following chain:
const username = preferred_username || nickname || name;
// Create the new user, as one didn't exist before!
user = await upsert(db, tenant, {
displayName,
user = await upsert(mongo, tenant, {
username,
role: GQLUSER_ROLE.COMMENTER,
email,
emailVerified: email_verified,
@@ -310,7 +320,7 @@ export default class OIDCStrategy extends Strategy {
const { clientID, clientSecret, authorizationURL, tokenURL } = integration;
// Construct the callbackURL from the request.
const callbackURL = reconstructURL(req, `/api/tenant/auth/oidc/callback`);
const callbackURL = reconstructURL(req, `/api/auth/oidc/callback`);
// Create a new OAuth2Strategy, where we pass the verify callback bound to
// this OIDCStrategy instance.
@@ -18,9 +18,8 @@ export interface SSOStrategyOptions {
export interface SSOUserProfile {
id: string;
email: string;
username?: string;
username: string;
avatar?: string;
displayName?: string;
}
export interface SSOToken {
@@ -38,7 +37,7 @@ export const SSOUserProfileSchema = Joi.object()
.optionalKeys(["avatar", "displayName"]);
export async function findOrCreateSSOUser(
db: Db,
mongo: Db,
tenant: Tenant,
integration: GQLSSOAuthIntegration,
token: SSOToken
@@ -49,7 +48,7 @@ export async function findOrCreateSSOUser(
}
// Unpack/validate the token content.
const { id, email, username, displayName, avatar }: SSOUserProfile = validate(
const { id, email, username, avatar }: SSOUserProfile = validate(
SSOUserProfileSchema,
token.user
);
@@ -60,7 +59,7 @@ export async function findOrCreateSSOUser(
};
// Try to lookup user given their id provided in the `sub` claim.
let user = await retrieveUserWithProfile(db, tenant.id, profile);
let user = await retrieveUserWithProfile(mongo, tenant.id, profile);
if (!user) {
if (!integration.allowRegistration) {
// Registration is disabled, so we can't create the user user here.
@@ -70,11 +69,8 @@ export async function findOrCreateSSOUser(
// FIXME: (wyattjoh) implement rules! Not all users should be able to create an account via this method.
// Create the new user, as one didn't exist before!
user = await upsert(db, tenant, {
user = await upsert(mongo, tenant, {
username,
// When the displayName is disabled on the tenant, the displayName will
// never be set (or even stored in the database).
displayName,
role: GQLUSER_ROLE.COMMENTER,
email,
avatar,
+9 -4
View File
@@ -13,12 +13,17 @@ export const tenantMiddleware = ({
}: MiddlewareOptions): RequestHandler => async (req, res, next) => {
try {
// Set Talk on the request.
req.talk = {
cache: {
if (!req.talk) {
req.talk = {};
}
// Set the Talk Tenant Cache on the request.
if (!req.talk.cache) {
req.talk.cache = {
// Attach the tenant cache to the request.
tenant: cache,
},
};
};
}
// Attach the tenant to the request.
const tenant = await cache.retrieveByDomain(req.hostname);
+3 -11
View File
@@ -4,7 +4,7 @@ import { AppOptions } from "talk-server/app";
import {
logoutHandler,
signupHandler,
} from "talk-server/app/handlers/api/tenant/auth/local";
} from "talk-server/app/handlers/api/auth/local";
import { noCacheMiddleware } from "talk-server/app/middleware/cacheHeaders";
import {
wrapAuthn,
@@ -33,11 +33,7 @@ export function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
const router = express.Router();
// Mount the logout handler.
router.delete(
"/",
options.passport.authenticate("jwt", { session: false }),
logoutHandler({ redis: app.redis })
);
router.delete("/", logoutHandler(app));
// Mount the Local Authentication handlers.
router.post(
@@ -45,11 +41,7 @@ export function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
express.json(),
wrapAuthn(options.passport, app.signingConfig, "local")
);
router.post(
"/local/signup",
express.json(),
signupHandler({ db: app.mongo, signingConfig: app.signingConfig })
);
router.post("/local/signup", express.json(), signupHandler(app));
// Mount the external auth integrations with middleware/handle wrappers.
wrapPath(app, options, router, "facebook");
+31 -9
View File
@@ -2,13 +2,16 @@ import express from "express";
import passport from "passport";
import { AppOptions } from "talk-server/app";
import { graphQLHandler } from "talk-server/app/handlers/api/graphql";
import { installHandler } from "talk-server/app/handlers/api/install";
import { versionHandler } from "talk-server/app/handlers/api/version";
import { JSONErrorHandler } from "talk-server/app/middleware/error";
import { errorLogger } from "talk-server/app/middleware/logging";
import { notFoundMiddleware } from "talk-server/app/middleware/notFound";
import { authenticate } from "talk-server/app/middleware/passport";
import { tenantMiddleware } from "talk-server/app/middleware/tenant";
import { createManagementRouter } from "./management";
import { createTenantRouter } from "./tenant";
import { createNewAuthRouter } from "./auth";
export interface RouterOptions {
/**
@@ -18,19 +21,38 @@ export interface RouterOptions {
passport: passport.Authenticator;
}
export async function createAPIRouter(app: AppOptions, options: RouterOptions) {
export function createAPIRouter(app: AppOptions, options: RouterOptions) {
// Create a router.
const router = express.Router();
// Configure the tenant routes.
router.use("/tenant", await createTenantRouter(app, options));
// Configure the management routes.
router.use("/management", await createManagementRouter(app));
// Configure the version route.
router.get("/version", versionHandler);
// Installation middleware.
router.use(
"/install",
express.json(),
tenantMiddleware({ cache: app.tenantCache, passNoTenant: true }),
installHandler(app)
);
// Tenant identification middleware. All requests going past this point can
// only proceed if there is a valid Tenant for the hostname.
router.use(tenantMiddleware({ cache: app.tenantCache }));
// Setup Passport middleware.
router.use(options.passport.initialize());
// Authenticate all requests made to this route. This will allow requests
// that are not authenticated pass through.
router.use(authenticate(options.passport));
// Setup auth routes.
router.use("/auth", createNewAuthRouter(app, options));
// Configure the GraphQL route.
router.use("/graphql", express.json(), graphQLHandler(app));
// General API error handler.
router.use(notFoundMiddleware);
router.use(errorLogger);
@@ -1,22 +0,0 @@
import express from "express";
import { AppOptions } from "talk-server/app";
import managementGraphMiddleware from "talk-server/graph/management/middleware";
export async function createManagementRouter(app: AppOptions) {
const router = express.Router();
// Management API
router.use(
"/graphql",
express.json(),
await managementGraphMiddleware({
schema: app.schemas.management,
config: app.config,
mongo: app.mongo,
i18n: app.i18n,
})
);
return router;
}
-55
View File
@@ -1,55 +0,0 @@
import express from "express";
import { AppOptions } from "talk-server/app";
import { tenantInstallHandler } from "talk-server/app/handlers/api/tenant/install";
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 { authenticate } from "talk-server/app/middleware/passport";
import { createNewAuthRouter } from "./auth";
export async function createTenantRouter(
app: AppOptions,
options: RouterOptions
) {
const router = express.Router();
// Tenant setup handler.
router.use(
"/install",
express.json(),
tenantInstallHandler({
config: app.config,
cache: app.tenantCache,
redis: app.redis,
mongo: app.mongo,
})
);
// Tenant identification middleware.
router.use(tenantMiddleware({ cache: app.tenantCache }));
// Setup Passport middleware.
router.use(options.passport.initialize());
// Setup auth routes.
router.use("/auth", createNewAuthRouter(app, options));
// Tenant API
router.use(
"/graphql",
express.json(),
// Any users may submit their GraphQL requests with authentication, this
// middleware will unpack their user into the request.
authenticate(options.passport),
tenantContext(app),
await tenantGraphMiddleware({
schema: app.schemas.tenant,
config: app.config,
})
);
return router;
}
+23 -32
View File
@@ -3,33 +3,30 @@ import path from "path";
import { AppOptions } from "talk-server/app";
import { noCacheMiddleware } from "talk-server/app/middleware/cacheHeaders";
import { cspTenantMiddleware } from "talk-server/app/middleware/csp/tenant";
import { installedMiddleware } from "talk-server/app/middleware/installed";
import playground from "talk-server/app/middleware/playground";
import { tenantMiddleware } from "talk-server/app/middleware/tenant";
import { RouterOptions } from "talk-server/app/router/types";
import logger from "talk-server/logger";
import { cspTenantMiddleware } from "talk-server/app/middleware/csp/tenant";
import { tenantMiddleware } from "talk-server/app/middleware/tenant";
import Entrypoints from "../helpers/entrypoints";
import { createAPIRouter } from "./api";
import { createClientTargetRouter } from "./client";
export async function createRouter(app: AppOptions, options: RouterOptions) {
export function createRouter(app: AppOptions, options: RouterOptions) {
// Create a router.
const router = express.Router();
router.use("/api", noCacheMiddleware, await createAPIRouter(app, options));
// Attach the API router.
router.use("/api", noCacheMiddleware, createAPIRouter(app, options));
// Attach the GraphiQL if enabled.
if (app.config.get("enable_graphiql")) {
attachGraphiQL(router, app);
}
router.use(tenantMiddleware({ cache: app.tenantCache, passNoTenant: true }));
router.use(cspTenantMiddleware);
const staticURI = app.config.get("static_uri");
// TODO: (wyattjoh) figure out a better way of referencing paths.
// Load the entrypoint manifest.
const manifest = path.join(
__dirname,
@@ -43,8 +40,20 @@ export async function createRouter(app: AppOptions, options: RouterOptions) {
"asset-manifest.json"
);
const entrypoints = Entrypoints.fromFile(manifest);
if (entrypoints) {
// Tenant identification middleware.
router.use(
tenantMiddleware({
cache: app.tenantCache,
passNoTenant: true,
})
);
// Add CSP headers to the request, which only apply when serving HTML content.
router.use(cspTenantMiddleware);
const staticURI = app.config.get("static_uri");
// Add the embed targets.
router.use(
"/embed/stream",
@@ -75,9 +84,7 @@ export async function createRouter(app: AppOptions, options: RouterOptions) {
router.use(
"/admin",
// If we aren't already installed, redirect the user to the install page.
installedMiddleware({
tenantCache: app.tenantCache,
}),
installedMiddleware(),
createClientTargetRouter({
staticURI,
cacheDuration: false,
@@ -90,7 +97,6 @@ export async function createRouter(app: AppOptions, options: RouterOptions) {
installedMiddleware({
redirectIfInstalled: true,
redirectURL: "/admin",
tenantCache: app.tenantCache,
}),
createClientTargetRouter({
staticURI,
@@ -104,7 +110,7 @@ export async function createRouter(app: AppOptions, options: RouterOptions) {
"/",
// Redirect the user to the install page if they are not, otherwise redirect
// them to the admin.
installedMiddleware({ tenantCache: app.tenantCache }),
installedMiddleware(),
(req, res, next) => res.redirect("/admin")
);
} else {
@@ -130,21 +136,6 @@ function attachGraphiQL(router: Router, app: AppOptions) {
);
}
// Tenant GraphiQL
router.get(
"/tenant/graphiql",
playground({
endpoint: "/api/tenant/graphql",
subscriptionEndpoint: "/api/tenant/live",
})
);
// Management GraphiQL
router.get(
"/management/graphiql",
playground({
endpoint: "/api/management/graphql",
subscriptionEndpoint: "/api/management/live",
})
);
// GraphiQL
router.get("/graphiql", playground({ endpoint: "/api/graphql" }));
}