Skip to content

Commit

Permalink
Merge pull request #4224 from coralproject/develop
Browse files Browse the repository at this point in the history
v8.0.3
  • Loading branch information
tessalt committed Apr 21, 2023
2 parents 9a998b4 + 66942cb commit c39c078
Show file tree
Hide file tree
Showing 35 changed files with 482 additions and 120 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coralproject/talk",
"version": "8.0.2",
"version": "8.0.3",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"sideEffects": [
Expand Down
2 changes: 1 addition & 1 deletion src/core/client/admin/permissions/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface PermissionContext {

const permissionMap: PermissionMap<AbilityType, PermissionContext> = {
CHANGE_ROLE: {
[GQLUSER_ROLE.ADMIN]: (ctx) => ctx?.user.role !== GQLUSER_ROLE.ADMIN,
[GQLUSER_ROLE.ADMIN]: () => true,
[GQLUSER_ROLE.MODERATOR]: (ctx) => {
if (!ctx) {
return true;
Expand Down
138 changes: 112 additions & 26 deletions src/core/client/admin/test/community/community-rtl.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {

import { createContext } from "../create";
import {
communityUsers,
disabledEmail,
disabledLocalAuth,
disabledLocalAuthAdminTargetFilter,
Expand All @@ -39,6 +38,19 @@ import {
users,
} from "../fixtures";

export const communityUsers = createFixture<GQLUsersConnection>({
edges: [
{ node: users.admins[0], cursor: users.admins[0].createdAt },
{ node: users.admins[1], cursor: users.admins[1].createdAt },
{ node: users.moderators[0], cursor: users.moderators[0].createdAt },
{ node: users.moderators[1], cursor: users.moderators[1].createdAt },
{ node: users.moderators[2], cursor: users.moderators[2].createdAt },
{ node: users.staff[0], cursor: users.staff[0].createdAt },
{ node: users.commenters[0], cursor: users.commenters[0].createdAt },
],
pageInfo: { endCursor: null, hasNextPage: false },
});

const adminViewer = users.admins[0];

const createTestRenderer = async (
Expand Down Expand Up @@ -260,31 +272,6 @@ it("change user role", async () => {
expect(resolvers.Mutation!.updateUserRole!.called).toBe(true);
});

it("no one may change an admins role", async () => {
const resolvers = createResolversStub<GQLResolver>({
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(() =>
createFixture<GQLUsersConnection>({
edges: users.admins,
nodes: users.admins,
})
),
},
});
await createTestRenderer({
resolvers,
});

const container = await screen.findByTestId("community-container");
expect(container).toBeDefined();
expect(within(container).getAllByRole("row").length).toEqual(
users.admins.length
);
expect(
within(container).queryByLabelText("Change role")
).not.toBeInTheDocument();
});

it("org mods may allocate site mods", async () => {
const orgModerator = users.moderators[0];
const commenter = users.commenters[0];
Expand Down Expand Up @@ -625,6 +612,105 @@ it("allows admins to promote site mods to org mod", async () => {
);
});

it("change user role", async () => {
const user = users.commenters[0];
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateUserRole: ({ variables }) => {
expectAndFail(variables).toMatchObject({
userID: user.id,
role: GQLUSER_ROLE.STAFF,
});
const userRecord = pureMerge<typeof user>(user, {
role: variables.role,
});
return {
user: userRecord,
};
},
},
});
await createTestRenderer({
resolvers,
});

const userRow = await screen.findByTestId(`community-row-${user.id}`);
const changeRoleButton = within(userRow).getByLabelText("Change role");
userEvent.click(changeRoleButton);

const popup = within(userRow).getByLabelText(
"A dropdown to change the user role"
);
const staffButton = await within(popup).findByRole("button", {
name: "Staff",
});
fireEvent.click(staffButton);

expect(resolvers.Mutation!.updateUserRole!.called).toBe(true);
});

it("only admins can demote other admins", async () => {
const viewer = users.moderators[0];
const adminUser = users.admins[1];

const resolvers = createResolversStub<GQLResolver>({
Query: {
viewer: () => viewer,
settings: () => settingsWithMultisite,
},
});
await createTestRenderer({
resolvers,
});

const userRow = await screen.findByTestId(`community-row-${adminUser.id}`);

const changeRoleButton = within(userRow).queryByLabelText("Change role");
expect(changeRoleButton).toBeNull();
});

it("allow admins to demote other admins", async () => {
const viewer = users.admins[0];
const adminUser = users.admins[1];

const resolvers = createResolversStub<GQLResolver>({
Query: {
viewer: () => viewer,
settings: () => settingsWithMultisite,
},
Mutation: {
updateUserRole: () => {
const userRecord = pureMerge<typeof adminUser>(adminUser, {
role: GQLUSER_ROLE.MODERATOR,
moderationScopes: undefined,
});
return {
user: userRecord,
};
},
},
});
await createTestRenderer({
resolvers,
});

const userRow = await screen.findByTestId(`community-row-${adminUser.id}`);
const changeRoleButton = within(userRow).getByLabelText("Change role");
userEvent.click(changeRoleButton);

const popup = within(userRow).getByLabelText(
"A dropdown to change the user role"
);
const orgModButton = within(popup).getByRole("button", {
name: "Organization Moderator",
});
fireEvent.click(orgModButton);

await waitFor(() =>
expect(resolvers.Mutation!.updateUserRole!.called).toBe(true)
);
});

// BOOKMARK: marcus, i think we just need to add a new field to these fixtures, maybe scopes.sites.id?
it("load more", async () => {
await createTestRenderer({
Expand Down
7 changes: 7 additions & 0 deletions src/core/client/admin/test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,13 @@ export const users = {
role: GQLUSER_ROLE.ADMIN,
ignoreable: false,
},
{
id: "user-admin-1",
username: "Not Markus",
email: "[email protected]",
role: GQLUSER_ROLE.ADMIN,
ignoreable: false,
},
],
baseUser
),
Expand Down
9 changes: 5 additions & 4 deletions src/core/client/stream/common/scrollToBeginning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ function scrollToBeginning(
customScrollContainer?: HTMLElement
) {
const tab = root.getElementById("tab-COMMENTS");
const scrollContainer = customScrollContainer ?? window;
if (tab) {
scrollContainer.scrollTo({
top: getElementWindowTopOffset(scrollContainer, tab),
});
if (customScrollContainer) {
tab.scrollIntoView();
} else {
window.scrollTo({ top: getElementWindowTopOffset(window, tab) });
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@ const CommentsLinks: FunctionComponent<Props> = ({
const { renderWindow, customScrollContainer } = useCoralContext();
const root = useShadowRootOrDocument();
const onGoToArticleTop = useCallback(() => {
if (customScrollContainer) {
customScrollContainer.scrollTo({ top: 0 });
}
renderWindow.scrollTo({ top: 0 });
}, [renderWindow]);
}, [renderWindow, customScrollContainer]);
const onGoToCommentsTop = useCallback(() => {
scrollToBeginning(root, renderWindow, customScrollContainer);
}, [root, renderWindow, customScrollContainer]);
Expand Down
6 changes: 1 addition & 5 deletions src/core/client/ui/helpers/getElementWindowTopOffset.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
/**
* Get elements top offset relative to the window.
*/
function getElementWindowTopOffset(
window: Window | React.RefObject<any>["current"],
element: Element
) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
function getElementWindowTopOffset(window: Window, element: Element) {
return element.getBoundingClientRect().top + window.scrollY;
}

Expand Down
12 changes: 12 additions & 0 deletions src/core/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,4 +425,16 @@ export enum ERROR_CODES {
* their username and the provided username has already been taken.
*/
USERNAME_ALREADY_EXISTS = "USERNAME_ALREADY_EXISTS",

/**
* UNABLE_TO_UPDATE_STORY_URL is thrown when a story already exists for
* a storyID and we were unable to update the url for that story ID.
*/
UNABLE_TO_UPDATE_STORY_URL = "UNABLE_TO_UPDATE_STORY_URL",

/**
* DATA_CACHING_NOT_AVAILABLE is thrown when someone tries to enact a data
* caching action when it is not available for that tenant.
*/
DATA_CACHING_NOT_AVAILABLE = "DATA_CACHING_NOT_AVAILABLE",
}
6 changes: 3 additions & 3 deletions src/core/common/permissions/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {
isSiteModerator,
PermissionsActionRuleTest,
} from "./types";
// admins can do whatever they want to anyone except other admins
// admins can perform any action, even demoting other admins
const adminsArePowerful: PermissionsActionRuleTest = ({ viewer, user }) => ({
applies: viewer.role === "ADMIN" && user.role !== "ADMIN",
reason: "Admins may change any non admin's role or scopes",
applies: viewer.role === "ADMIN",
reason: "Admins may change any user's role or scopes",
});

// org mods can promote anyone < site mod to be an unscoped member
Expand Down
5 changes: 0 additions & 5 deletions src/core/common/permissions/validateRoleChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@ export const validateRoleChange = (
*/
scoped = false
): boolean => {
// User is admin
if (user.role === "ADMIN") {
return false;
}

// Viewer is changing their own role
if (user.id === viewer.id) {
return false;
Expand Down
11 changes: 10 additions & 1 deletion src/core/server/data/cache/commentActionsCache.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import { Logger } from "coral-server/logger";
import { CommentAction } from "coral-server/models/action/comment";
import { AugmentedRedis } from "coral-server/services/redis";
import { TenantCache } from "coral-server/services/tenant/cache";

import { MongoContext } from "../context";
import { dataCacheAvailable, IDataCache } from "./dataCache";

export class CommentActionsCache {
export class CommentActionsCache implements IDataCache {
private expirySeconds: number;

private mongo: MongoContext;
private redis: AugmentedRedis;
private tenantCache: TenantCache | null;
private logger: Logger;

private commentActionsByKey: Map<string, Readonly<CommentAction>>;

constructor(
mongo: MongoContext,
redis: AugmentedRedis,
tenantCache: TenantCache | null,
logger: Logger,
expirySeconds: number
) {
this.mongo = mongo;
this.redis = redis;
this.tenantCache = tenantCache;
this.logger = logger.child({ dataCache: "CommentActionsCache" });

this.expirySeconds = expirySeconds;

this.commentActionsByKey = new Map<string, Readonly<CommentAction>>();
}

public async available(tenantID: string): Promise<boolean> {
return dataCacheAvailable(this.tenantCache, tenantID);
}

private computeDataKey(tenantID: string, id: string) {
const key = `${tenantID}:${id}:commentActionData`;
return key;
Expand Down
1 change: 1 addition & 0 deletions src/core/server/data/cache/commentCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const createFixtures = async (
const commentCache = new CommentCache(
mongo,
redis,
null,
logger,
false,
options?.expirySeconds ? options.expirySeconds : 5 * 60
Expand Down
Loading

0 comments on commit c39c078

Please sign in to comment.