mirror of
https://github.com/wassname/talk.git
synced 2026-07-28 11:27:05 +08:00
[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:
+13
-23
@@ -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,
|
||||
}),
|
||||
};
|
||||
})
|
||||
);
|
||||
+18
-12
@@ -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,
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user