Skip to content

Commit

Permalink
Merge pull request #10013 from hicommonwealth/release/v1.7.4-x
Browse files Browse the repository at this point in the history
Release/v1.7.4-4
  • Loading branch information
ilijabojanovic authored Nov 22, 2024
2 parents 5853926 + 68646d6 commit 3a58888
Show file tree
Hide file tree
Showing 24 changed files with 260 additions and 344 deletions.
1 change: 1 addition & 0 deletions libs/adapters/src/trpc/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export enum Tag {
DiscordBot = 'DiscordBot',
Token = 'Token',
Contest = 'Contest',
Poll = 'Poll',
}

export type Commit<Input extends ZodSchema, Output extends ZodSchema> = (
Expand Down
23 changes: 16 additions & 7 deletions libs/model/src/community/CreateGroup.command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InvalidInput, type Command } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { PermissionEnum } from '@hicommonwealth/schemas';
import { Op } from 'sequelize';
import { models, sequelize } from '../database';
import { authRoles } from '../middleware';
Expand Down Expand Up @@ -70,13 +71,21 @@ export function CreateGroup(): Command<typeof schemas.CreateGroup> {

if (group.id) {
// add topic level interaction permissions for current group
const groupPermissions = (payload.topics || []).map((t) => ({
group_id: group.id!,
topic_id: t.id,
allowed_actions: sequelize.literal(
`ARRAY[${t.permissions.map((p) => `'${p}'`).join(', ')}]::"enum_GroupPermissions_allowed_actions"[]`,
) as unknown as schemas.PermissionEnum[],
}));
const groupPermissions = (payload.topics || []).map((t) => {
const permissions = t.permissions;
// Enable UPDATE_POLL by default for all group permissions
// TODO: remove once client supports selecting the UPDATE_POLL permission
permissions.push(PermissionEnum.UPDATE_POLL);
return {
group_id: group.id!,
topic_id: t.id,
allowed_actions: sequelize.literal(
`ARRAY[${permissions
.map((p) => `'${p}'`)
.join(', ')}]::"enum_GroupPermissions_allowed_actions"[]`,
) as unknown as schemas.PermissionEnum[],
};
});
await models.GroupPermission.bulkCreate(groupPermissions, {
transaction,
});
Expand Down
7 changes: 6 additions & 1 deletion libs/model/src/community/UpdateGroup.command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InvalidInput, type Command } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { PermissionEnum } from '@hicommonwealth/schemas';
import { Op } from 'sequelize';
import { models, sequelize } from '../database';
import { authRoles } from '../middleware';
Expand Down Expand Up @@ -90,10 +91,14 @@ export function UpdateGroup(): Command<typeof schemas.UpdateGroup> {
// update topic level interaction permissions for current group
await Promise.all(
(payload.topics || [])?.map(async (t) => {
const permissions = t.permissions;
if (!permissions.includes(PermissionEnum.UPDATE_POLL)) {
permissions.push(PermissionEnum.UPDATE_POLL);
}
if (group.id) {
await models.GroupPermission.update(
{
allowed_actions: t.permissions,
allowed_actions: permissions,
},
{
where: {
Expand Down
1 change: 1 addition & 0 deletions libs/model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * as DiscordBot from './discordBot';
export * as Email from './emails';
export * as Feed from './feed';
export * as LoadTest from './load-testing';
export * as Poll from './poll';
export * as Reaction from './reaction';
export * as Snapshot from './snapshot';
export * as Subscription from './subscription';
Expand Down
57 changes: 53 additions & 4 deletions libs/model/src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Context,
InvalidActor,
InvalidInput,
InvalidState,
} from '@hicommonwealth/core';
import {
Address,
Expand All @@ -12,6 +13,8 @@ import {
CommentContextInput,
Group,
GroupPermissionAction,
PollContext,
PollContextInput,
ReactionContext,
ReactionContextInput,
ThreadContext,
Expand Down Expand Up @@ -81,7 +84,7 @@ async function findThread(
thread,
author_address_id: thread.address_id,
community_id: thread.community_id,
topic_id: thread.topic_id ?? undefined,
topic_id: thread.topic_id,
is_collaborator,
};
}
Expand Down Expand Up @@ -117,6 +120,17 @@ async function findReaction(
};
}

async function findPoll(actor: Actor, poll_id: number) {
const poll = await models.Poll.findOne({
where: { id: poll_id },
});
if (!poll) {
throw new InvalidInput('Must provide a valid poll id to authorize');
}

return poll;
}

async function findAddress(
actor: Actor,
community_id: string,
Expand Down Expand Up @@ -206,9 +220,11 @@ async function hasTopicPermissions(
}
>(
`
SELECT g.*, gp.topic_id, gp.allowed_actions
FROM "Groups" as g JOIN "GroupPermissions" gp ON g.id = gp.group_id
WHERE g.community_id = :community_id AND gp.topic_id = :topic_id
SELECT g.*, gp.topic_id, gp.allowed_actions
FROM "Groups" as g
JOIN "GroupPermissions" gp ON g.id = gp.group_id
WHERE g.community_id = :community_id
AND gp.topic_id = :topic_id
`,
{
type: QueryTypes.SELECT,
Expand Down Expand Up @@ -499,3 +515,36 @@ export function authReaction() {
await mustBeAuthorized(ctx, { author: true });
};
}

export function authPoll({ action }: AggregateAuthOptions) {
return async (ctx: Context<typeof PollContextInput, typeof PollContext>) => {
const poll = await findPoll(ctx.actor, ctx.payload.poll_id);
const threadAuth = await findThread(ctx.actor, poll.thread_id, false);
const { address, is_author } = await findAddress(
ctx.actor,
threadAuth.community_id,
['admin', 'moderator', 'member'],
);

if (threadAuth.thread.archived_at)
throw new InvalidState('Thread is archived');
(ctx as { context: PollContext }).context = {
address,
is_author,
poll,
poll_id: poll.id!,
community_id: threadAuth.community_id,
thread: threadAuth.thread,
};

await mustBeAuthorized(ctx, {
author: true,
permissions: action
? {
topic_id: threadAuth.topic_id,
action,
}
: undefined,
});
};
}
13 changes: 12 additions & 1 deletion libs/model/src/middleware/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
import {
AuthContext,
CommentContext,
PollContext,
ThreadContext,
} from '@hicommonwealth/schemas';
import moment from 'moment';
import type { AddressInstance, ThreadInstance } from '../models';
import type { AddressInstance, PollInstance, ThreadInstance } from '../models';

const log = logger(import.meta);

Expand Down Expand Up @@ -116,6 +117,16 @@ export function mustBeAuthorizedComment(
};
}

export function mustBeAuthorizedPoll(actor: Actor, context?: PollContext) {
if (!context?.address) throw new InvalidActor(actor, 'Not authorized');
if (!context?.poll) throw new InvalidActor(actor, 'Not authorized poll');
return context as PollContext & {
address: AddressInstance;
poll: PollInstance;
thread: ThreadInstance;
};
}

/**
* Guards for starting and ending dates to be in a valid date range
* @param start_date start date
Expand Down
23 changes: 3 additions & 20 deletions libs/model/src/models/poll.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
import { Poll } from '@hicommonwealth/schemas';
import Sequelize from 'sequelize';
import type { CommunityAttributes } from './community';
import type { ThreadAttributes } from './thread';
import { z } from 'zod';
import type { ModelInstance } from './types';
import type { VoteAttributes } from './vote';

export type PollAttributes = {
id: number;
community_id: string;
thread_id: number;
prompt: string;
options: string;
ends_at: Date;

created_at?: Date;
updated_at?: Date;
last_commented_on?: Date;

// associations
Thread?: ThreadAttributes;
Community?: CommunityAttributes;
votes?: VoteAttributes[];
};
export type PollAttributes = z.infer<typeof Poll>;

export type PollInstance = ModelInstance<PollAttributes>;

Expand Down
17 changes: 3 additions & 14 deletions libs/model/src/models/vote.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import { Vote } from '@hicommonwealth/schemas';
import Sequelize from 'sequelize';
import type { PollAttributes } from './poll';
import { z } from 'zod';
import type { ModelInstance } from './types';

export type VoteAttributes = {
poll_id: number;
option: string;
address: string;
author_community_id: string;
community_id: string;
id?: number;
created_at?: Date;
updated_at?: Date;

// associations
poll?: PollAttributes;
};
export type VoteAttributes = z.infer<typeof Vote>;

export type VoteInstance = ModelInstance<VoteAttributes>;

Expand Down
45 changes: 45 additions & 0 deletions libs/model/src/poll/createPollVote.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Command, InvalidState } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import moment from 'moment/moment';
import { models } from '../database';
import { authPoll } from '../middleware';
import { mustBeAuthorizedPoll } from '../middleware/guards';

export const CreateVotePollErrors = {
InvalidOption: 'Invalid response option',
PollingClosed: 'Polling already finished',
};

export function CreatePollVote(): Command<typeof schemas.CreatePollVote> {
return {
...schemas.CreatePollVote,
auth: [
authPoll({
action: 'UPDATE_POLL',
}),
],
body: async ({ actor, payload, context }) => {
const { poll, address } = mustBeAuthorizedPoll(actor, context);
if (
!poll.ends_at &&
moment(poll.ends_at).utc().isBefore(moment().utc())
) {
throw new InvalidState(CreateVotePollErrors.PollingClosed);
}

// TODO: migrate this to be JSONB array of strings in the DB
const options = JSON.parse(poll.options);
if (!options.includes(payload.option)) {
throw new InvalidState(CreateVotePollErrors.InvalidOption);
}

return models.Vote.create({
poll_id: payload.poll_id,
address: address.address,
author_community_id: address.community_id,
community_id: poll.community_id,
option: payload.option,
});
},
};
}
1 change: 1 addition & 0 deletions libs/model/src/poll/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './createPollVote.command';
1 change: 1 addition & 0 deletions libs/schemas/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './community.schemas';
export * from './contest.schemas';
export * from './discord.schemas';
export * from './load-testing.schemas';
export * from './poll.schemas';
export * from './quest.schemas';
export * from './snapshot.schemas';
export * from './subscription.schemas';
Expand Down
14 changes: 14 additions & 0 deletions libs/schemas/src/commands/poll.schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PollContext } from '@hicommonwealth/schemas';
import { z } from 'zod';
import { Vote } from '../entities/poll.schemas';
import { PG_INT } from '../utils';

export const CreatePollVote = {
input: z.object({
thread_id: PG_INT,
poll_id: z.number(),
option: z.string(),
}),
output: Vote,
context: PollContext,
};
11 changes: 10 additions & 1 deletion libs/schemas/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { Address, Comment, Reaction, Thread, Topic } from './entities';
import { Address, Comment, Poll, Reaction, Thread, Topic } from './entities';

// Input schemas for authorization context
export const AuthContextInput = z.object({ community_id: z.string() });
Expand All @@ -10,6 +10,7 @@ export const ReactionContextInput = z.object({
community_id: z.string(),
reaction_id: z.number(),
});
export const PollContextInput = z.object({ poll_id: z.number() });

/**
* Base authorization context
Expand Down Expand Up @@ -77,3 +78,11 @@ export const ReactionContext = z.object({
reaction: Reaction,
});
export type ReactionContext = z.infer<typeof ReactionContext>;

export const PollContext = z.object({
...AuthContext.shape,
...PollContextInput.shape,
poll: Poll,
thread: Thread,
});
export type PollContext = z.infer<typeof PollContext>;
1 change: 1 addition & 0 deletions libs/schemas/src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './group-permission.schemas';
export * from './group.schemas';
export * from './launchpad.schemas';
export * from './notification.schemas';
export * from './poll.schemas';
export * from './quest.schemas';
export * from './reaction.schemas';
export * from './snapshot.schemas';
Expand Down
36 changes: 36 additions & 0 deletions libs/schemas/src/entities/poll.schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from 'zod';
import { Community } from '../entities/community.schemas';
import { Thread } from '../entities/thread.schemas';
import { PG_INT } from '../utils';

const _vote = z.object({
id: PG_INT.optional(),
poll_id: z.number(),
option: z.string(),
address: z.string(),
author_community_id: z.string(),
community_id: z.string(),
created_at: z.coerce.date().optional(),
updated_at: z.coerce.date().optional(),
});

export const Poll = z.object({
id: PG_INT.optional(),
community_id: z.string(),
thread_id: z.number(),
prompt: z.string(),
options: z.string(),
ends_at: z.coerce.date(),
created_at: z.coerce.date().optional(),
updated_at: z.coerce.date().optional(),

// associations
Thread: Thread.optional(),
Community: Community.optional(),
votes: _vote.optional(),
});

export const Vote = _vote.extend({
// associations
poll: Poll.optional(),
});
Loading

0 comments on commit 3a58888

Please sign in to comment.