[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
@@ -90,7 +90,7 @@ export const spam: IntermediateModerationPhase = async ({
user_agent: userAgent, // REQUIRED
comment_content: comment.body,
permalink: story.url,
comment_author: author.displayName || author.username || "",
comment_author: author.username || "",
comment_type: "comment",
is_test: false,
});
+3 -4
View File
@@ -4,6 +4,7 @@ import { Bearer } from "permit";
import uuid from "uuid/v4";
import { Config } from "talk-server/config";
import { AuthenticationError } from "talk-server/errors";
import { User } from "talk-server/models/user";
import { Request } from "talk-server/types/express";
@@ -78,8 +79,7 @@ export function createJWTSigningConfig(config: Config): JWTSigningConfig {
return createAsymmetricSigningConfig(algorithm, secret);
}
// TODO: (wyattjoh) return better error.
throw new Error("invalid algorithm specified");
throw new AuthenticationError(`invalid algorithm=${algorithm} specified`);
}
export type SigningTokenOptions = Pick<
@@ -137,7 +137,6 @@ export async function revokeJWT(redis: Redis, jti: string, validFor: number) {
export async function checkJWTRevoked(redis: Redis, jti: string) {
const expiredAtString = await redis.get(generateJTIRevokedKey(jti));
if (expiredAtString) {
// TODO: (wyattjoh) return a better error.
throw new Error("JWT was revoked");
throw new AuthenticationError("JWT was revoked");
}
}
+36 -18
View File
@@ -16,12 +16,7 @@ export type AugmentedRedis = Omit<Redis, "pipeline"> &
pipeline(commands?: string[][]): AugmentedPipeline;
};
function configureRedisClient(redis: Redis) {
// Attach to the error event.
redis.on("error", (err: Error) => {
logger.error({ err }, "an error occurred with redis");
});
function augmentRedisClient(redis: Redis): AugmentedRedis {
// mhincrby will increment many hash values.
redis.defineCommand("mhincrby", {
numberOfKeys: 1,
@@ -31,28 +26,51 @@ function configureRedisClient(redis: Redis) {
end
`,
});
return redis as AugmentedRedis;
}
/**
* create will connect to the Redis instance identified in the configuration.
*
* @param config application configuration.
*/
export async function createRedisClient(
config: Config
): Promise<AugmentedRedis> {
function attachHandlers(redis: Redis) {
// Attach to the error event.
redis.on("error", (err: Error) => {
logger.error({ err }, "an error occurred with redis");
});
}
export function createRedisClient(
config: Config,
lazyConnect: boolean = false
): Redis {
try {
const redis = new RedisClient(config.get("redis"), {
lazyConnect: true,
lazyConnect,
});
// Configure the redis client for use with the custom commands.
configureRedisClient(redis);
// Configure the redis client with the handlers for logging
attachHandlers(redis);
return redis;
} catch (err) {
throw new InternalError(err, "could not connect to redis");
}
}
/**
* createAugmentedRedisClient will connect to the Redis instance identified in
* the configuration.
*
* @param config application configuration.
*/
export async function createAugmentedRedisClient(
config: Config
): Promise<AugmentedRedis> {
try {
const redis = augmentRedisClient(createRedisClient(config, true));
// Connect the redis client.
await redis.connect();
return redis as AugmentedRedis;
return redis;
} catch (err) {
throw new InternalError(err, "could not connect to redis");
}
+13 -7
View File
@@ -38,8 +38,7 @@ import {
UpdateStoryInput,
} from "talk-server/models/story";
import { Tenant } from "talk-server/models/tenant";
import Task from "talk-server/queue/Task";
import { ScraperData } from "talk-server/queue/tasks/scraper";
import { ScraperQueue } from "talk-server/queue/tasks/scraper";
import { scrape } from "talk-server/services/stories/scraper";
import { AugmentedRedis } from "../redis";
@@ -47,10 +46,10 @@ import { AugmentedRedis } from "../redis";
export type FindOrCreateStory = FindOrCreateStoryInput;
export async function findOrCreate(
db: Db,
mongo: Db,
tenant: Tenant,
input: FindOrCreateStory,
scraper: Task<ScraperData>
scraper: ScraperQueue
) {
// If the URL is provided, and the url is not on a allowed domain, then refuse
// to create the Asset.
@@ -63,7 +62,7 @@ export async function findOrCreate(
// TODO: check to see if the tenant has enabled lazy story creation, if they haven't, switch to find only.
const story = await findOrCreateStory(db, tenant.id, input);
const story = await findOrCreateStory(mongo, tenant.id, input);
if (!story) {
return null;
}
@@ -186,16 +185,22 @@ export async function create(
tenant: Tenant,
storyID: string,
storyURL: string,
input: CreateStory
{ metadata }: CreateStory
) {
// Ensure that the given URL is allowed.
if (!isURLPermitted(tenant, storyURL)) {
throw new StoryURLInvalidError({ storyURL, tenantDomains: tenant.domains });
}
// Construct the input payload.
const input: CreateStoryInput = { metadata };
if (metadata) {
input.scrapedAt = new Date();
}
// Create the story in the database.
let newStory = await createStory(mongo, tenant.id, storyID, storyURL, input);
if (!input.metadata && !newStory.scrapedAt) {
if (!metadata) {
// If the scraper has not scraped this story and story metadata was not
// provided, we need to scrape it now!
newStory = await scrape(mongo, tenant.id, newStory.id);
@@ -324,6 +329,7 @@ export async function merge(
"updated destination story with new comment counts"
);
// Remove the stories from MongoDB.
const { deletedCount } = await removeStories(mongo, tenant.id, sourceIDs);
log.debug({ deletedStories: deletedCount }, "deleted source stories");
-43
View File
@@ -8,7 +8,6 @@ import {
USERNAME_REGEX,
} from "talk-common/helpers/validate";
import {
DisplayNameExceedsMaxLengthError,
EmailAlreadySetError,
EmailExceedsMaxLengthError,
EmailInvalidFormatError,
@@ -32,7 +31,6 @@ import {
setUserLocalProfile,
setUserUsername,
updateUserAvatar,
updateUserDisplayName,
updateUserEmail,
updateUserPassword,
updateUserRole,
@@ -71,25 +69,6 @@ function validateUsername(tenant: Tenant, username: string) {
}
}
const DISPLAY_NAME_MAX_LENGTH = USERNAME_MAX_LENGTH;
/**
* validateDisplayName will validate that the username is valid.
*
* @param tenant tenant where the User is associated with
* @param displayName the display name to be tested
*/
function validateDisplayName(tenant: Tenant, displayName: string) {
// TODO: replace these static regex/length with database options in the Tenant eventually
if (displayName.length > DISPLAY_NAME_MAX_LENGTH) {
throw new DisplayNameExceedsMaxLengthError(
displayName.length,
DISPLAY_NAME_MAX_LENGTH
);
}
}
/**
* validatePassword will validate that the password is valid. Current
* implementation uses a length statically, future versions will expose this as
@@ -355,28 +334,6 @@ export async function updateUsername(
return updateUserUsername(mongo, tenant.id, userID, username);
}
/**
* updateDisplayName will update a given User's display name.
*
* @param mongo mongo database to interact with
* @param tenant Tenant where the User will be interacted with
* @param userID the User's ID that we are updating
* @param displayName the display name that we are setting on the User
*/
export async function updateDisplayName(
mongo: Db,
tenant: Tenant,
userID: string,
displayName?: string
) {
if (displayName) {
// Validate the display name.
validateDisplayName(tenant, displayName);
}
return updateUserDisplayName(mongo, tenant.id, userID, displayName);
}
/**
* updateRole will update the given User to the specified role.
*