Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions api/apollo-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ const server = new ProtectedApolloServer({
req.login(data, err => (err ? rej(err) : res()))
),
user: currentUser,
getImageSignatureExpiration: () => {
/*
Expire images sent to the client at midnight each day (UTC).
Expiration needs to be consistent across all images in order
to preserve client-side caching abilities and to prevent checksum
mismatches during SSR
*/
const date = new Date();
date.setHours(24);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date.getTime();
},
};
},
subscriptions: {
Expand Down
1 change: 1 addition & 0 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { Loader } from './loaders/types';
export type GraphQLContext = {
user: DBUser,
updateCookieUserData: (data: DBUser) => Promise<void>,
getImageSignatureExpiration: () => number,
loaders: {
[key: string]: Loader,
},
Expand Down
12 changes: 12 additions & 0 deletions api/queries/community/coverPhoto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @flow
import type { GraphQLContext } from '../../';
import type { DBCommunity } from 'shared/types';
import { signImageUrl } from 'shared/imgix';

export default ({ coverPhoto }: DBCommunity, _: any, ctx: GraphQLContext) => {
return signImageUrl(coverPhoto, {
w: 1280,
h: 384,
expires: ctx.getImageSignatureExpiration(),
});
};
4 changes: 4 additions & 0 deletions api/queries/community/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import watercooler from './watercooler';
import brandedLogin from './brandedLogin';
import slackSettings from './slackSettings';
import joinSettings from './joinSettings';
import coverPhoto from './coverPhoto';
import profilePhoto from './profilePhoto';

// no-op resolvers to transition while removing payments
import type { DBCommunity } from 'shared/types';
Expand Down Expand Up @@ -67,6 +69,8 @@ module.exports = {
brandedLogin,
slackSettings,
joinSettings,
coverPhoto,
profilePhoto,

invoices,
recurringPayments,
Expand Down
12 changes: 12 additions & 0 deletions api/queries/community/profilePhoto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @flow
import type { GraphQLContext } from '../../';
import type { DBUser } from 'shared/types';
import { signImageUrl } from 'shared/imgix';

export default ({ profilePhoto }: DBUser, _: any, ctx: GraphQLContext) => {
return signImageUrl(profilePhoto, {
w: 256,
h: 256,
expires: ctx.getImageSignatureExpiration(),
});
};
8 changes: 8 additions & 0 deletions api/queries/message/content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @flow
import type { GraphQLContext } from '../../';
import type { DBMessage } from 'shared/types';
import body from './content/body';

export default (message: DBMessage, _: any, ctx: GraphQLContext) => ({
body: body(message, ctx.getImageSignatureExpiration()),
});
9 changes: 9 additions & 0 deletions api/queries/message/content/body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @flow
import type { DBMessage } from 'shared/types';
import { signImageUrl } from 'shared/imgix';

export default (message: DBMessage, imageSignatureExpiration: number) => {
const { content, messageType } = message;
if (messageType !== 'media') return content.body;
return signImageUrl(content.body, { expires: imageSignatureExpiration });
};
2 changes: 2 additions & 0 deletions api/queries/message/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import author from './author';
import thread from './thread';
import reactions from './reactions';
import parent from './parent';
import content from './content';

module.exports = {
Query: {
Expand All @@ -18,5 +19,6 @@ module.exports = {
thread,
reactions,
parent,
content,
},
};
22 changes: 4 additions & 18 deletions api/queries/thread/content.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
// @flow
import type { GraphQLContext } from '../../';
import type { DBThread } from 'shared/types';
import body from './content/body';

export default ({ content }: DBThread, _: any, ctx: GraphQLContext) => {
const defaultDraftState = JSON.stringify({
blocks: [
{
key: 'foo',
text: '',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
],
entityMap: {},
});

export default (thread: DBThread, _: any, ctx: GraphQLContext) => {
return {
title: content.title,
body: content.body ? content.body : defaultDraftState,
title: thread.content.title,
body: body(thread, ctx.getImageSignatureExpiration()),
};
};
44 changes: 44 additions & 0 deletions api/queries/thread/content/body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @flow
import type { DBThread } from 'shared/types';
import { signImageUrl } from 'shared/imgix';

export default (thread: DBThread, imageSignatureExpiration: number) => {
const { content } = thread;

if (!content.body) {
return JSON.stringify({
blocks: [
{
key: 'foo',
text: '',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
],
entityMap: {},
});
}

// Replace the local image srcs with the remote image src
const body = JSON.parse(content.body);

const imageKeys = Object.keys(body.entityMap).filter(
key => body.entityMap[key].type.toLowerCase() === 'image'
);

imageKeys.forEach((key, index) => {
if (!body.entityMap[key[index]]) return;

const { src } = body.entityMap[imageKeys[index]].data;

// transform the body inline with signed image urls
body.entityMap[imageKeys[index]].data.src = signImageUrl(src, {
expires: imageSignatureExpiration,
});
});

return JSON.stringify(body);
};
79 changes: 18 additions & 61 deletions api/queries/thread/rootThread.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,30 @@
// @flow
import { calculateThreadScoreQueue } from 'shared/bull/queues';
import type { GraphQLContext } from '../../';
import { canViewThread } from '../../utils/permissions';

export default async (
_: any,
{ id }: { id: string },
{ loaders, user }: GraphQLContext
) => {
const thread = await loaders.thread.load(id);

// if a thread wasn't found
if (!thread) return null;

/*
If no user exists, we need to make sure the thread being fetched is not in a private channel
*/
if (!user) {
const [channel, community] = await Promise.all([
loaders.channel.load(thread.channelId),
loaders.community.load(thread.communityId),
]);

// if the channel is private, don't return any thread data
if (!channel || !community || channel.isPrivate || community.isPrivate)
return null;
return thread;
} else {
// if the user is signed in, we need to check if the channel is private as well as the user's permission in that channel
const [
channelPermissions,
channel,
communityPermissions,
community,
] = await Promise.all([
loaders.userPermissionsInChannel.load([user.id, thread.channelId]),
loaders.channel.load(thread.channelId),
loaders.userPermissionsInCommunity.load([user.id, thread.communityId]),
loaders.community.load(thread.communityId),
]);

// if the thread is in a private channel where the user is not a member, don't return any thread data
if (!channel || !community) return null;
if (
channel.isPrivate &&
(!channelPermissions || !channelPermissions.isMember)
)
return null;
if (
community.isPrivate &&
(!communityPermissions || !communityPermissions.isMember)
)
return null;
if (!(await canViewThread(user ? user.id : 'undefined', id, loaders))) {
return null;
}

// If the threads score hasn't been updated in the past
// 24 hours add a new job to the queue to update it
if (
(!thread.score && !thread.scoreUpdatedAt) ||
(thread.scoreUpdatedAt &&
Date.now() > new Date(thread.scoreUpdatedAt).getTime() + 86400000)
) {
calculateThreadScoreQueue.add(
{
threadId: thread.id,
},
{
jobId: thread.id,
}
);
}
return thread;
const thread = await loaders.thread.load(id);
// If the threads score hasn't been updated in the past
// 24 hours add a new job to the queue to update it
if (
(!thread.score && !thread.scoreUpdatedAt) ||
(thread.scoreUpdatedAt &&
Date.now() > new Date(thread.scoreUpdatedAt).getTime() + 86400000)
) {
calculateThreadScoreQueue.add(
{ threadId: thread.id },
{ jobId: thread.id }
);
}

return thread;
};
20 changes: 8 additions & 12 deletions api/queries/user/coverPhoto.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
// @flow
import type { GraphQLContext } from '../../';
import type { DBUser } from 'shared/types';
import ImgixClient from 'imgix-core-js';
let imgix = new ImgixClient({
host: 'spectrum-imgp.imgix.net',
secureURLToken: 'asGmuMn5yq73B3cH',
});
import { signImageUrl } from 'shared/imgix';

export default ({ coverPhoto }: DBUser) => {
// if the image is not being served from our S3 imgix source, serve it from our web proxy
if (coverPhoto && coverPhoto.indexOf('spectrum.imgix.net') < 0) {
return imgix.buildURL(coverPhoto, { w: 640, h: 192 });
}
// if the image is being served from the S3 imgix source, return that url
return coverPhoto;
export default ({ coverPhoto }: DBUser, _: any, ctx: GraphQLContext) => {
return signImageUrl(coverPhoto, {
w: 1280,
h: 384,
expires: ctx.getImageSignatureExpiration(),
});
};
20 changes: 8 additions & 12 deletions api/queries/user/profilePhoto.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
// @flow
import type { GraphQLContext } from '../../';
import type { DBUser } from 'shared/types';
import ImgixClient from 'imgix-core-js';
let imgix = new ImgixClient({
host: 'spectrum-imgp.imgix.net',
secureURLToken: 'asGmuMn5yq73B3cH',
});
import { signImageUrl } from 'shared/imgix';

export default ({ profilePhoto }: DBUser) => {
// if the image is not being served from our S3 imgix source, serve it from our web proxy
if (profilePhoto && profilePhoto.indexOf('spectrum.imgix.net') < 0) {
return imgix.buildURL(profilePhoto, { w: 128, h: 128 });
}
// if the image is being served from the S3 imgix source, return that url
return profilePhoto;
export default ({ profilePhoto }: DBUser, _: any, ctx: GraphQLContext) => {
return signImageUrl(profilePhoto, {
w: 256,
h: 256,
expires: ctx.getImageSignatureExpiration(),
});
};
19 changes: 18 additions & 1 deletion api/test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,24 @@ export const request = (query: mixed, { context, variables }: Options = {}) => {
schema,
query,
undefined,
{ loaders: createLoaders(), ...context },
{
loaders: createLoaders(),
getImageSignatureExpiration: () => {
/*
Expire images sent to the client at midnight each day (UTC).
Expiration needs to be consistent across all images in order
to preserve client-side caching abilities and to prevent checksum
mismatches during SSR
*/
const date = new Date();
date.setHours(24);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
return date.getTime();
},
...context,
},
variables
);
};
15 changes: 4 additions & 11 deletions api/utils/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,10 @@ AWS.config.update({
});
const s3 = new AWS.S3();

const generateImageUrl = path => {
// remove the bucket name from the path
const newPath = path.replace('spectrum-chat/', '');

// this is the default source for our imgix account, which starts
// at the bucket root, thus we remove the bucket from the path
const imgixBase = 'https://spectrum.imgix.net';

// return a new url to update the user object
return imgixBase + '/' + newPath;
};
// remove the bucket name from the url
// the bucket name is not required since it is automatically bound
// to our imgix source
const generateImageUrl = path => path.replace('spectrum-chat/', '');

export const uploadImage = async (
file: FileUpload,
Expand Down
Loading